diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e117764a36fda..bbc66c4bb46ce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,7 @@ /packages/shared-data/pricing.ts @roryw10 @kevcodez /packages/shared-data/plans.ts @roryw10 @kevcodez +/apps/docs/pages/ @supabase/developers /studio/ @supabase/Dashboard /apps/www/ @supabase/Website /docker/ @supabase/cli diff --git a/.github/workflows/studio-tests.yml b/.github/workflows/studio-tests.yml index 611ac60fafdcb..45c73f91bbcc8 100644 --- a/.github/workflows/studio-tests.yml +++ b/.github/workflows/studio-tests.yml @@ -30,7 +30,6 @@ jobs: node-version: [18.x] cmd: - npm run test:studio - - npm run build:studio steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 15fb3d390c678..fd3feee51ed62 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -12,7 +12,8 @@ concurrency: jobs: typecheck: - runs-on: ubuntu-latest + # Uses larger hosted runner as it significantly decreases build times + runs-on: [larger-runner-4cpu] strategy: matrix: diff --git a/.prettierignore b/.prettierignore index 9de82e2c6c51d..1435554ced11c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,4 +19,3 @@ studio/.storybook studio/.swc studio/.turbo studio/node -studio/data \ No newline at end of file diff --git a/apps/docs/components/MDX/project_setup.mdx b/apps/docs/components/MDX/project_setup.mdx index 83fbaff74c9e3..50e887cb50408 100644 --- a/apps/docs/components/MDX/project_setup.mdx +++ b/apps/docs/components/MDX/project_setup.mdx @@ -44,7 +44,7 @@ supabase db pull - When working locally you can run the followng command to create a new migration file: + When working locally you can run the following command to create a new migration file: ```bash diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 8f0def19cbf01..73ec0aeaf086f 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1018,6 +1018,7 @@ export const ai = { { name: 'Engineering for scale', url: '/guides/ai/engineering-for-scale' }, { name: 'Choosing Compute Add-on', url: '/guides/ai/choosing-compute-addon' }, { name: 'Going to Production', url: '/guides/ai/going-to-prod' }, + { name: 'RAG with Permissions', url: '/guides/ai/rag-with-permissions' }, ], }, { @@ -1256,6 +1257,10 @@ export const platform: NavMenuConstant = { name: 'Build a Supabase Integration', url: '/guides/platform/oauth-apps/build-a-supabase-integration', }, + { + name: 'OAuth Scopes', + url: '/guides/platform/oauth-apps/oauth-scopes', + }, ], }, { diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts index 18aec08d3265e..6181b97204ae9 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts @@ -17,12 +17,19 @@ export function deepFilterSections( section.type === 'markdown' || specFunctionIds.includes(section.id) ) - .map((section) => { + .flatMap((section) => { if ('items' in section) { - return { - ...section, - items: deepFilterSections(section.items, specFunctionIds), + const items = deepFilterSections(section.items, specFunctionIds) + + // Only include this category (heading) if it has subitems + if (items.length > 0) { + return { + ...section, + items, + } } + + return [] } return section }) diff --git a/apps/docs/data/nav/supabase-js/v2.ts b/apps/docs/data/nav/supabase-js/v2.ts index f71236ae3aaae..1ad4fed975177 100644 --- a/apps/docs/data/nav/supabase-js/v2.ts +++ b/apps/docs/data/nav/supabase-js/v2.ts @@ -132,6 +132,7 @@ const Nav = [ { name: 'removeChannel()', url: '/reference/javascript/removechannel', items: [] }, { name: 'removeAllChannels()', url: '/reference/javascript/removeallchannels', items: [] }, { name: 'getChannels()', url: '/reference/javascript/getchannels', items: [] }, + { name: 'broadcastMessage()', url: '/reference/javascript/broadcastmessage', items: [] }, ], }, { diff --git a/apps/docs/pages/guides/ai/rag-with-permissions.mdx b/apps/docs/pages/guides/ai/rag-with-permissions.mdx new file mode 100644 index 0000000000000..cf0dcf4aa7b3d --- /dev/null +++ b/apps/docs/pages/guides/ai/rag-with-permissions.mdx @@ -0,0 +1,278 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'ai-rag-with-permissions', + title: 'RAG with Permissions', + subtitle: 'Fine-grain access control with Retrieval Augmented Generation.', + description: 'Implement fine-grain access control with retrieval augmented generation', + sidebar_label: 'RAG with Permissions', +} + +Since pgvector is built on top of Postgres, you can implement fine-grain access control on your vector database using [Row Level Security (RLS)](/docs/guides/database/postgres/row-level-security). This means you can restrict which documents are returned during a vector similarity search to users that have access to them. Supabase also supports [Foreign Data Wrappers (FDW)](/docs/guides/database/extensions/wrappers/overview) which means you can use an external database or data source to determine these permissions if your user data doesn't exist in Supabase. + +Use this guide to learn how to restrict access to documents when performing retrieval augmented generation (RAG). + +## Example + +In a typical RAG setup, your documents are chunked into small subsections and similarity is performed over those sections: + +```sql +-- Track documents/pages/files/etc +create table documents ( + id bigint primary key generated always as identity, + name text not null, + owner_id uuid not null references auth.users (id) default auth.uid(), + created_at timestamp with time zone not null default now() +); + +-- Store the content and embedding vector for each section in the document +-- with a reference to original document (one-to-many) +create table document_sections ( + id bigint primary key generated always as identity, + document_id bigint not null references documents (id), + content text not null, + embedding vector (384) +); +``` + +Notice how we record the `owner_id` on each document. Let's create an RLS policy that restricts access to `document_sections` based on whether or not they own the linked document: + +```sql +-- enable row level security +alter table document_sections enable row level security; + +-- setup RLS for select operations +create policy "Users can query their own document sections" +on document_sections for select to authenticated using ( + document_id in ( + select id + from documents + where owner_id = auth.uid() + ) +); +``` + + + +In this example, the current user is determined using the built-in `auth.uid()` function when the query is executed through your project's auto-generated [REST API](/docs/guides/api). If you are connecting to your Supabase database through a direct Postgres connection, see [Direct Postgres Connection](#direct-postgres-connection) below for directions on how to achieve the same access control. + + + +Now every `select` query executed on `document_sections` will implicitly filter the returned sections based on whether or not the current user has access to them. + +For example, executing: + +```sql +select * from document_sections; +``` + +as an authenticated user will only return rows that they are the owner of (as determined by the linked document). More importantly, semantic search over these sections (or any additional filtering for that matter) will continue to respect these RLS policies: + +```sql +-- Perform inner product similarity based on a match_threshold +select * +from document_sections +where document_sections.embedding <#> embedding < -match_threshold +order by document_sections.embedding <#> embedding; +``` + +The above example only configures `select` access to users. If you wanted, you could create more RLS policies for inserts, updates, and deletes in order to apply the same permission logic for those other operations. See [Row Level Security](/docs/guides/database/postgres/row-level-security) for a more in-depth guide on RLS policies. + +## Alternative Scenarios + +Every app has its own unique requirements and may differ from the above example. Here are some alternative scenarios we often see and how they are implemented in Supabase. + +### Documents owned by multiple people + +Instead of a one-to-many relationship between `users` and `documents`, you may require a many-to-many relationship so that multiple people can access the same document. Let's reimplement this using a join table: + +```sql +create table document_owners ( + id bigint primary key generated always as identity, + owner_id uuid not null references auth.users (id) default auth.uid(), + document_id bigint not null references documents (id) +); +``` + +Then your RLS policy would change to: + +```sql +create policy "Users can query their own document sections" +on document_sections for select to authenticated using ( + document_id in ( + select document_id + from document_owners + where owner_id = auth.uid() + ) +); +``` + +Instead of directly querying the `documents` table, we query the join table. + +### User and document data live outside of Supabase + +You may have an existing system that stores users, documents, and their permissions in a separate database. Let's explore the scenario where this data exists in another Postgres database. We'll use a foreign data wrapper (FDW) to connect to the external DB from within your Supabase DB: + + + +RLS is latency-sensitive, so extra caution should be taken before implementing this method. Use the [query plan analyzer](https://supabase.com/docs/guides/platform/performance#optimizing-poor-performing-queries) to measure execution times for your queries to ensure they are within expected ranges. For enterprise applications, contact enterprise@supabase.io. + + + + + +For data sources other than Postgres, see [Foreign Data Wrappers](/docs/guides/database/extensions/wrappers/overview) for a list of external sources supported today. If your data lives in a source not provided in the list, please contact [support](https://supabase.com/dashboard/support/new) and we'll be happy to discuss your use case. + + + +Let's assume your external DB contains a `users` and `documents` table like this: + +```sql +create table public.users ( + id bigint primary key generated always as identity, + email text not null, + created_at timestamp with time zone not null default now() +); + +create table public.documents ( + id bigint primary key generated always as identity, + name text not null, + owner_id bigint not null references public.users (id), + created_at timestamp with time zone not null default now() +); +``` + +In your Supabase DB, let's create foreign tables that link to the above tables: + +```sql +create schema external; +create extension postgres_fdw with schema extensions; + +-- Setup the foreign server +create server foreign_server + foreign data wrapper postgres_fdw + options (host '', port '', dbname ''); + +-- Map local 'authenticated' role to external 'postgres' user +create user mapping for authenticated + server foreign_server + options (user 'postgres', password ''); + +-- Import foreign 'users' and 'documents' tables into 'external' schema +import foreign schema public limit to (users, documents) + from server foreign_server into external; +``` + + + +This example maps the `authenticated` role in Supabase to the `postgres` user in the external DB. In production, it's best to create a custom user on the external DB that has the minimum permissions necessary to access the information you need. + +On the Supabase DB, we use the built-in `authenticated` role which is automatically used when end users make authenticated requests over your auto-generated REST API. If you plan to connect to your Supabase DB over a direct Postgres connection instead of the REST API, you can change this to any user you like. See [Direct Postgres Connection](#direct-postgres-connection) for more info. + + + +We'll store `document_sections` and their embeddings in Supabase so that we can perform similarity search over them via pgvector. + +```sql +create table document_sections ( + id bigint primary key generated always as identity, + document_id bigint not null, + content text not null, + embedding vector (384) +); +``` + +We maintain a reference to the foreign document via `document_id`, but without a foreign key reference since foreign keys can only be added to local tables. Be sure to use the same ID data type that you use on your external documents table. + +Since we're managing users and authentication outside of Supabase, we have two options: + +1. Make a direct Postgres connection to the Supabase DB and set the current user every request +2. Issue a custom JWT from your system and use it to authenticate with the REST API + +#### Direct Postgres Connection + +You can directly connect to your Supabase Postgres DB using the [connection info](/dashboard/project/_/settings/database) on your project's database settings page. To use RLS with this method, we use a custom session variable that contains the current user's ID: + +```sql +-- enable row level security +alter table document_sections enable row level security; + +-- setup RLS for select operations +create policy "Users can query their own document sections" +on document_sections for select to authenticated using ( + document_id in ( + select id + from external.documents + where owner_id = current_setting('app.current_user_id')::bigint + ) +); +``` + +The session variable is accessed through the `current_setting()` function. We name the variable `app.current_user_id` here, but you can modify this to any name you like. We also cast it to a `bigint` since that was the data type of the `user.id` column. Change this to whatever data type you use for your ID. + +Now for every request, we set the user's ID at the beginning of the session: + +```sql +set app.current_user_id = ''; +``` + +Then all subsequent queries will inherit the permission of that user: + +```sql +-- Only document sections owned by the user are returned +select * +from document_sections +where document_sections.embedding <#> embedding < -match_threshold +order by document_sections.embedding <#> embedding; +``` + + + +You might be tempted to discard RLS completely and simply filter by user within the `where` clause. Though this will work, we recommend RLS as a general best practice since RLS is always applied even as new queries and application logic is introduced in the future. + + + +#### Custom JWT with REST API + +If you would like to use the auto-generated REST API to query your Supabase database using JWTs from an external auth provider, you can get your auth provider to issue a custom JWT for Supabase. + +See the [Clerk Supabase docs](https://clerk.com/docs/integrations/databases/supabase) for an example of how this can be done. Modify the instructions to work with your own auth provider as needed. + +Now we can simply use the same RLS policy from our first example: + +```sql +-- enable row level security +alter table document_sections enable row level security; + +-- setup RLS for select operations +create policy "Users can query their own document sections" +on document_sections for select to authenticated using ( + document_id in ( + select id + from documents + where owner_id = auth.uid() + ) +); +``` + +Under the hood, `auth.uid()` references `current_setting('request.jwt.claim.sub')` which corresponds to the JWT's `sub` (subject) claim. This setting is automatically set at the beginning of each request to the REST API. + +All subsequent queries will inherit the permission of that user: + +```sql +-- Only document sections owned by the user are returned +select * +from document_sections +where document_sections.embedding <#> embedding < -match_threshold +order by document_sections.embedding <#> embedding; +``` + +### Other scenarios + +There are endless approaches to this problem based on the complexities of each system. Luckily Postgres comes with all the primitives needed to provide access control in the way that works best for your project. + +If the examples above didn't fit your use case or you need to adjust them slightly to better fit your existing system, feel free to reach out to [support](https://supabase.com/dashboard/support/new) and we'll be happy to assist you. + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/auth.mdx b/apps/docs/pages/guides/auth.mdx index 2a516ff35ea5c..b92f5c23ab400 100644 --- a/apps/docs/pages/guides/auth.mdx +++ b/apps/docs/pages/guides/auth.mdx @@ -104,7 +104,7 @@ With policies, your database becomes the rules engine. Instead of repetitively f ```js const loggedInUserId = 'd0714948' -let { data, error } = await supabase +const { data, error } = await supabase .from('users') .select('user_id, name') .eq('user_id', loggedInUserId) @@ -116,7 +116,7 @@ let { data, error } = await supabase ... you can simply define a rule on your database table, `auth.uid() = user_id`, and your request will return the rows which pass the rule, even when you remove the filter from your middleware: ```js -let { data, error } = await supabase.from('users').select('user_id, name') +const { data, error } = await supabase.from('users').select('user_id, name') // console.log(data) // Still => { id: 'd0714948', name: 'Jane' } diff --git a/apps/docs/pages/guides/auth/auth-captcha.mdx b/apps/docs/pages/guides/auth/auth-captcha.mdx index baa4184f96d36..d43dade2f2b82 100644 --- a/apps/docs/pages/guides/auth/auth-captcha.mdx +++ b/apps/docs/pages/guides/auth/auth-captcha.mdx @@ -177,7 +177,7 @@ We will pass it the sitekey we copied from the Cloudflare website as a property ```jsx { setCaptchaToken(token) } /> ``` diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index 9f81599860372..1f28fab4f4dd4 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -339,7 +339,7 @@ We need to create an event listener in the root `+layout.svelte` file in order t export let data - let { supabase, session } = data + const { supabase, session } = data $: ({ supabase, session } = data) onMount(() => { @@ -368,7 +368,7 @@ We can access the supabase instance in our `+page.svelte` file through the data diff --git a/apps/docs/pages/guides/auth/auth-mfa.mdx b/apps/docs/pages/guides/auth/auth-mfa.mdx index 8ad610634fa0c..7494dd3a0db1b 100644 --- a/apps/docs/pages/guides/auth/auth-mfa.mdx +++ b/apps/docs/pages/guides/auth/auth-mfa.mdx @@ -573,7 +573,7 @@ If your application uses the Supabase Database, Storage or Edge Functions, just ### Why is there a challenge and verify API when challenge does not do much? -TOTP is not going to be the only MFA factor Supabase Auth is going to support in the future. By separating out the challenge and verify steps, we're making the library forward compatible with new factors we may add in the future -- such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would actually send out the SMS with the authentication code. +TOTP is not going to be the only MFA factor Supabase Auth is going to support in the future. By separating out the challenge and verify steps, we're making the library forward compatible with new factors we may add in the future -- such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would actually send out the SMS with the authentication code. For convenience, you may use `challengeAndVerify` to create and verify a challenge in a single step. ### What's inside the QR code? diff --git a/apps/docs/pages/guides/auth/phone-login/messagebird.mdx b/apps/docs/pages/guides/auth/phone-login/messagebird.mdx index 659b108e57f65..68e0918f7a8bd 100644 --- a/apps/docs/pages/guides/auth/phone-login/messagebird.mdx +++ b/apps/docs/pages/guides/auth/phone-login/messagebird.mdx @@ -49,13 +49,11 @@ You will need the following values to get started: - Live API Key / Test API Key - MessageBird originator -Now go to the Auth > Settings page in the Supabase dashboard (https://supabase.com/dashboard/project/YOUR-PROJECT-REF/auth/settings). +Now go to the [Auth > Providers](https://supabase.com/dashboard/project/_/auth/providers) page in the Supabase dashboard and select "Phone" from the Auth Providers list. -You should see an option to enable Phone Signup. +You should see an option to enable the Phone provider. -![Enable Phone Sign-Up](/docs/img/guides/auth-twilio/7.png) - -Toggle it on, and copy the 2 values over from the messagebird dashboard. Click save. +Toggle it on, and copy the 2 values over from the Messagebird dashboard. Click save. Note: If you use the Test API Key, the OTP will not be delivered to the mobile number specified but messagebird will log the response in the dashboard. If the Live API Key is used instead, the OTP will be delivered and there will be a deduction in your free credits. @@ -68,7 +66,7 @@ Now the backend should be setup, we can proceed to add our client-side code! The SMS message sent to a phone containing an OTP code can be customized. This is useful if you need to mention a brand name or display a website address. -Go to Auth > Templates page in the Supabase dashboard (https://supabase.com/dashboard/project/YOUR-PROJECT-REF/auth/templates). +Go to [Auth > Templates](https://supabase.com/dashboard/project/_/auth/templates) page in the Supabase dashboard. Use the variable `.Code` in the template to display the code. @@ -88,7 +86,7 @@ In this use scenario we'll be using the user's mobile phone number as an alterna Using supabase-js on the client you'll want to use the same `signUp` method that you'd use for email based sign ups, but with the `phone` param instead of the `email param`: ```js -let { user, error } = await supabase.auth.signUp({ +const { user, error } = await supabase.auth.signUp({ phone: '+13334445555', password: 'some-password', }) @@ -134,7 +132,7 @@ The user will now receive an SMS with a 6-digit pin that you will need to receiv You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`: ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '+13334445555', token: '123456', }) @@ -195,7 +193,7 @@ Also now that the mobile has been verified, the user can use the number and pass ```js -let { user, error } = await supabase.auth.signInWithPassword({ +const { user, error } = await supabase.auth.signInWithPassword({ phone: '+13334445555', password: 'some-password', }) @@ -243,7 +241,7 @@ In this scenario you are granting your user's the ability to login to their acco In JavaScript we can use the `signIn` method with a single parameter: `phone` ```js -let { user, error } = await supabase.auth.signInWithOtp({ +const { user, error } = await supabase.auth.signInWithOtp({ phone: '+13334445555', }) ``` @@ -286,7 +284,7 @@ The second step is the same as the previous section, you need to collect the 6-d ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '+13334445555', token: '123456', }) diff --git a/apps/docs/pages/guides/auth/phone-login/twilio.mdx b/apps/docs/pages/guides/auth/phone-login/twilio.mdx index 4f9f7e721f2fe..dfc1df1a47a12 100644 --- a/apps/docs/pages/guides/auth/phone-login/twilio.mdx +++ b/apps/docs/pages/guides/auth/phone-login/twilio.mdx @@ -94,13 +94,11 @@ You should now be able to see all three values you'll need to get started: ![All the credentials you'll need](/docs/img/guides/auth-twilio/6.png) -Now go to the Auth > Providers page in the Supabase dashboard and select "Phone" from the Auth Providers list (https://supabase.com/dashboard/project/_/auth/providers). +Now go to the [Auth > Providers](https://supabase.com/dashboard/project/_/auth/providers) page in the Supabase dashboard and select "Phone" from the Auth Providers list. -You should see an option to enable Phone Signup: +You should see an option to enable the Phone provider. -![Enable Phone Sign-Up](/docs/img/guides/auth-twilio/7.png) - -Toggle it on, and copy the 3 values over from the twilio dashboard. Click save. +Toggle it on, and copy the 3 values over from the Twilio dashboard. Click save. Note: for "Twilio Message Service SID" you can use the Sender Phone Number generated above. @@ -112,7 +110,7 @@ Now the backend should be setup, we can proceed to add our client-side code! The SMS message sent to a phone containing an OTP code can be customized. This is useful if you need to mention a brand name or display a website address. -Go to Auth > Providers page (under "Configuration") in the Supabase dashboard and select "Phone" from the Auth Providers list (https://supabase.com/dashboard/project/_/auth/providers). Scroll to the very bottom of the "Phone" section to the "SMS Message" input - you can customize the SMS message here. +Go to the [Auth > Providers](https://supabase.com/dashboard/project/_/auth/providers) page in the Supabase dashboard and select "Phone" from the Auth Providers list. Scroll to the very bottom of the "Phone" section to the "SMS Message" input - you can customize the SMS message here. Use the variable `.Code` in the template to display the OTP code. Here's an example in the SMS template. @@ -134,7 +132,7 @@ In this scenario we'll be using the user's mobile phone number and a correspondi Using supabase-js on the client you'll want to use the same `signUp` method that you'd use for email based sign ups, but with the `phone` param instead of the `email param`: ```js -let { user, error } = await supabase.auth.signUp({ +const { user, error } = await supabase.auth.signUp({ phone: '+13334445555', password: 'some-password', }) @@ -180,7 +178,7 @@ The user will now receive an SMS with a 6-digit pin that you will need to receiv You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`: ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '+13334445555', token: '123456', type: 'sms', @@ -242,7 +240,7 @@ Also now that the mobile has been verified, the user can use the number and pass ```js -let { user, error } = await supabase.auth.signInWithPassword({ +const { user, error } = await supabase.auth.signInWithPassword({ phone: '+13334445555', password: 'some-password', }) @@ -290,7 +288,7 @@ In this scenario you are granting your user's the ability to login to their acco In JavaScript we can use the `signIn` method with a single parameter: `phone` ```js -let { user, error } = await supabase.auth.signInWithOtp({ +const { user, error } = await supabase.auth.signInWithOtp({ phone: '+13334445555', }) ``` @@ -333,7 +331,7 @@ The second step is the same as the previous section, you need to collect the 6-d ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '+13334445555', token: '123456', type: 'sms', @@ -429,7 +427,7 @@ There is no change in the verification process, you should continue to use the ` ```js // After receiving a WhatsApp OTP -let { data, error } = await supabase.auth.verifyOtp({ +const { data, error } = await supabase.auth.verifyOtp({ phone: '+57336567365', token: '123456', type: 'sms', diff --git a/apps/docs/pages/guides/auth/phone-login/vonage.mdx b/apps/docs/pages/guides/auth/phone-login/vonage.mdx index f04563c97de74..addaed8d57e6b 100644 --- a/apps/docs/pages/guides/auth/phone-login/vonage.mdx +++ b/apps/docs/pages/guides/auth/phone-login/vonage.mdx @@ -49,11 +49,11 @@ Select the country you want a number for. You will need a mobile phone number wi ### Configure Supabase -Now go to the Auth > Settings page in the Supabase dashboard (https://supabase.com/dashboard/project/YOUR-PROJECT-REF/auth/settings). +Now go to the [Auth > Providers](https://supabase.com/dashboard/project/_/auth/providers) page in the Supabase dashboard and select "Phone" from the Auth Providers list. -You should see an option to enable Phone Signup. +You should see an option to enable the Phone provider. -Toggle it on, and copy the api key, api secret and phone number values over from the Vonage dashboard. Click save. +Toggle it on, and copy the API key, API secret and phone number values over from the Vonage dashboard. Click save. Now the backend should be setup, we can proceed to add our client-side code! @@ -61,7 +61,7 @@ Now the backend should be setup, we can proceed to add our client-side code! The SMS message sent to a phone containing an OTP code can be customized. This is useful if you need to mention a brand name or display a website address. -Go to Auth > Templates page in the Supabase dashboard (https://supabase.com/dashboard/project/YOUR-PROJECT-REF/auth/templates). +Go to [Auth > Templates](https://supabase.com/dashboard/project/_/auth/templates) page in the Supabase dashboard. Use the variable `.Code` in the template to display the code. @@ -81,7 +81,7 @@ In this use scenario we'll be using the user's mobile phone number as an alterna Using supabase-js on the client you'll want to use the same `signUp` method that you'd use for email based sign ups, but with the `phone` param instead of the `email param`: ```js -let { user, error } = await supabase.auth.signUp({ +const { user, error } = await supabase.auth.signUp({ phone: '491512223334444', password: 'some-password', }) @@ -127,7 +127,7 @@ The user will now receive an SMS with a 6-digit pin that you will need to receiv You should present a form to the user so they can input the 6 digit pin, then send it along with the phone number to `verifyOtp`: ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '491512223334444', token: '123456', }) @@ -188,7 +188,7 @@ Also now that the mobile has been verified, the user can use the number and pass ```js -let { user, error } = await supabase.auth.signInWithPassword({ +const { user, error } = await supabase.auth.signInWithPassword({ phone: '491512223334444', password: 'some-password', }) @@ -236,7 +236,7 @@ In this scenario you are granting your user's the ability to login to their acco In JavaScript we can use the `signIn` method with a single parameter: `phone` ```js -let { user, error } = await supabase.auth.signInWithOtp({ +const { user, error } = await supabase.auth.signInWithOtp({ phone: '491512223334444', }) ``` @@ -279,7 +279,7 @@ The second step is the same as the previous section, you need to collect the 6-d ```js -let { session, error } = await supabase.auth.verifyOtp({ +const { session, error } = await supabase.auth.verifyOtp({ phone: '491512223334444', token: '123456', }) diff --git a/apps/docs/pages/guides/auth/server-side-rendering.mdx b/apps/docs/pages/guides/auth/server-side-rendering.mdx index 775cf4fa5f3d2..4ec0547391fff 100644 --- a/apps/docs/pages/guides/auth/server-side-rendering.mdx +++ b/apps/docs/pages/guides/auth/server-side-rendering.mdx @@ -40,7 +40,7 @@ You can configure [redirects URLs](https://supabase.com/dashboard/project/_/auth -Supabase Auth supports two authentication flows: **Implicit** and **PKCE**. The **PKCE** flow is generally preferred when on the server. It introduces a few additional steps which guard a against replay and URL capture attacks. Unlike the implicit flow, it also allows users to access the `access_token` and `refresh_token` on the server. +Supabase Auth supports two authentication flows: **Implicit** and **PKCE**. The **PKCE** flow is generally preferred when on the server. It introduces a few additional steps which guard against replay and URL capture attacks. Unlike the implicit flow, it also allows users to access the `access_token` and `refresh_token` on the server. .supabase.co/auth/v1/callback` -- Click "Register" at the bottom of the form. +- Specify a _Web_ _Redirect URI_. It should should look like this: `https://.supabase.co/auth/v1/callback` +- Finally, select _Register_ at the bottom of the screen. ![Register an application.](/docs/img/guides/auth-azure/azure-register-app.png) -## Obtain a Client ID +## Obtain a Client ID and Secret -This will serve as the `client_id` when you make API calls to authenticate the user. +- Once your app has been registered, the client ID can be found under the [list of app registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) under the column titled _Application (client) ID_. +- You can also find it in the app overview screen. +- Place the Client ID in the Azure configuration screen in the Supabase AUth dashboard. -- Once your app has been registered, the client id can be found under the [list of app registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) under the column titled "Application (client) ID". +![Obtain the client ID](/docs/img/guides/auth-azure/azure-client-id.png) -![Obtain the client id](/docs/img/guides/auth-azure/azure-client-id.png) +- Select _Add a certificate or secret_ in the app overview screen and open the _Client secrets_ tab. +- Select _New client secret_ to create a new client secret. +- Choose a preferred expiry time of the secret. Make sure you record this in your calendar days in advance so you have enough time to create a new one without suffering from any downtime. +- Once the secret is generated place the _Value_ column (not _Secret ID_) in the Azure configuration screen in the Supabase Auth dashboard. -## Obtain a Secret ID - -This will serve as the `client_secret` when you make API calls to authenticate the user. +![Obtain the client secret](/docs/img/guides/auth-azure/azure-client-secret.png) -- Click on the name of the app registered above. -- Under "Essentials", click on "Client credentials". -- Navigate to the "Client secrets" tab and select "New client secret". -- Enter a description and choose your preferred expiry for the secret. -- Once the secret is generated, save the `value` (not the secret ID). +## Guarding Against Unverified Email Domains + +Microsoft Entra ID had a vulnerability that allowed tenants to configure any email address for their users without verification. This property could be used to gain access to already existing user accounts in Supabase Auth when a multi-tenant OAuth application was registered and Login with Azure was active. + +Supabase Auth projects _are only affected if_: + +- You use a single-tenant OAuth application +- You have explicitly configured your OAuth application to send ID tokens with unverified email addresses to Supabase Auth + +However, it is strongly recommended that you configure the [additional `xms_edov` claim](https://learn.microsoft.com/en-us/azure/active-directory/develop/migrate-off-email-claim-authorization#using-the-xms_edov-optional-claim-to-determine-email-verification-status-and-migrate-users) so that Supabase Auth will be able to pick up any issues that may appear in the future. This claim, when configured, gives an indication whether the email address sent to Supabase Auth from Azure is verified or not. + +Configure this by: + +- Select the _App registrations_ menu in Microsoft Entra ID on the Azure portal. +- Select the OAuth app. +- Select the _Manifest_ menu in the sidebar. +- Make a backup of the JSON just in case. +- Identify the `optionalClaims` key. +- Edit it by specifying the following object: + ```json + "optionalClaims": { + "idToken": [ + { + "name": "xms_edov", + "source": null, + "essential": false, + "additionalProperties": [] + }, + { + "name": "email", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "accessToken": [ + { + "name": "xms_edov", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + ``` +- Select _Save_ to apply the new configuraion. -![Obtain the client secret](/docs/img/guides/auth-azure/azure-client-secret.png) +## Configure a Tenant URL (Optional) -## Obtain the Tenant URL (Optional) +A Microsoft Entra tenant is the directory of users who are allowed to access your project. This section depends on what your OAuth registration uses for _Supported account types._ -The default tenant url is `https://login.microsoftonline.com/common`. This will allow users with personal Azure accounts as well as with Azure work accounts, other than that of your own organization, to sign in to your app. This is set by default in Supabase and you can leave the `Azure Tenant URL` field blank in your provider settings. +By default, Supabase Auth uses the _common_ Microsoft tenant (`https://login.microsoftonline.com/common`) which generally allows any Microsoft account to sign in to your project. Microsoft Entra further limits what accounts can access your project depending on the type of OAuth application you registered. -If you want to allow users from **your Azure organization only** to be able to log in to you app +If your app is registered as _My organization only_ for the _Supported account types_ you may want to configure Supabase Auth with the organization's tenant URL. This will use the tenant's authorization flows instead, and will limit access at the Supabase Auth level to Microsoft accounts arising from only the specified tenant. -- Select the Directory (Tenant) ID value -- Set the tenant URL in your Supabase Azure Provider settings to `https://login.microsoftonline.com/` +Configure this by storing a value under _Azure Tenant URL_ in the Supabase Auth provider configuration page for Azure that has the following format `https://login.microsoftonline.com/`. ## Add login code to your client app -Supabase Auth requires that Azure returns a valid email address. Therefore you must request the `email` scope in the `signIn` method above. +Supabase Auth requires that Azure returns a valid email address. Therefore you must request the `email` scope in the `signInWithOAuth` method. @@ -99,9 +141,9 @@ When your user signs in, call [loginWith(Provider)](/docs/reference/kotlin/auth- ```kotlin suspend fun signInWithAzure() { - supabase.gotrue.loginWith(Azure) { - scopes.add("email") - } + supabase.gotrue.loginWith(Azure) { + scopes.add("email") + } } ``` @@ -132,7 +174,7 @@ When your user signs out, call [logout()](/docs/reference/kotlin/auth-signout) t ```kotlin suspend fun signOut() { - supabase.gotrue.logout() + supabase.gotrue.logout() } ``` @@ -168,9 +210,9 @@ async function signInWithAzure() { ```kotlin suspend fun signInWithAzure() { - supabase.gotrue.loginWith(Azure) { - scopes.add("offline_access") - } + supabase.gotrue.loginWith(Azure) { + scopes.add("offline_access") + } } ``` @@ -181,6 +223,7 @@ suspend fun signInWithAzure() { - [Azure Developer Account](https://portal.azure.com) - [GitHub Discussion](https://github.com/supabase/gotrue/pull/54#issuecomment-757043573) +- [Potential Risk of Privilege Escalation in Azure AD Applications](https://msrc.microsoft.com/blog/2023/06/potential-risk-of-privilege-escalation-in-azure-ad-applications/) export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/auth/social-login/auth-google.mdx b/apps/docs/pages/guides/auth/social-login/auth-google.mdx index be1ca53e2a4e8..fcf03da9931c1 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-google.mdx @@ -234,11 +234,11 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf ## Using Native Sign in with Google in Flutter - Unlike the OAuth flow which requires the use of a web browser, the native Sign in with Google flow on Android uses the [operating system's built-in functionalities](https://developers.google.com/android/reference/com/google/android/gms/auth/api/identity/package-summary) to prompt the user for consent. Note that native sign-in has been rebranded as _One Tap sign-in on Android_ by Google, which you should not confuse with _One Tap sign in for web_, as mentioned below. + Native Google sign in with Supabase is done through [flutter_appauth](https://pub.dev/packages/flutter_appauth) package for iOS and [google_sign_in](https://pub.dev/packages/google_sign_in) package for Android. When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. - If you are building a Flutter app, you can use the [flutter_appauth](https://pub.dev/packages/flutter_appauth) package to sign a user into your Supabase project: + If you are building a Flutter app, you can use the [google_sign_in](https://pub.dev/packages/google_sign_in) package for Android and [flutter_appauth](https://pub.dev/packages/flutter_appauth) package for iOS to sign a user into your Supabase project: ```dart import 'dart:convert'; @@ -246,6 +246,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf import 'package:crypto/crypto.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; + import 'package:google_sign_in/google_sign_in.dart'; /// Function to generate a random 16 character string. String _generateRandomString() { @@ -254,70 +255,95 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf } Future signInWithGoogle() { - // Just a random string - final rawNonce = _generateRandomString(); - final hashedNonce = - sha256.convert(utf8.encode(rawNonce)).toString(); - - /// TODO: update the client ID with your own + /// TODO: update the iOS and Web client ID with your own. /// /// Client ID that you registered with Google Cloud. - /// You will have two different values for iOS and Android. - const clientId = 'YOUR_CLIENT_ID_HERE'; - - /// reverse DNS form of the client ID + `:/` is set as the redirect URL - final redirectUrl = '${clientId.split('.').reversed.join('.')}:/'; - - /// Fixed value for google login - const discoveryUrl = - 'https://accounts.google.com/.well-known/openid-configuration'; - - final appAuth = FlutterAppAuth(); + /// Note that in order to perform Google sign in on Android, you need to + /// provide the web client ID, not the Android client ID. + final clientId = Platform.isIOS ? 'IOS_CLIENT_ID' : 'WEB_CLIENT_ID'; + + late final String? idToken; + late final String? accessToken; + String? rawNonce; + + // Use AppAuth to perform Google sign in on iOS + // and use GoogleSignIn package for Google sign in on Android + if (Platform.isIOS) { + const appAuth = FlutterAppAuth(); + + // Just a random string + rawNonce = _generateRandomString(); + final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); + + /// Set as reversed DNS form of Google Client ID + `:/` for Google login + final redirectUrl = '${clientId.split('.').reversed.join('.')}:/'; + + /// Fixed value for google login + const discoveryUrl = + 'https://accounts.google.com/.well-known/openid-configuration'; + + // authorize the user by opening the concent page + final result = await appAuth.authorize( + AuthorizationRequest( + clientId, + redirectUrl, + discoveryUrl: discoveryUrl, + nonce: hashedNonce, + scopes: [ + 'openid', + 'email', + 'profile', + ], + ), + ); + + if (result == null) { + throw 'No result'; + } - // authorize the user by opening the consent page - final result = await appAuth.authorize( - AuthorizationRequest( - clientId, - redirectUrl, - discoveryUrl: discoveryUrl, - nonce: hashedNonce, + // Request the access and id token to google + final tokenResult = await appAuth.token( + TokenRequest( + clientId, + redirectUrl, + authorizationCode: result.authorizationCode, + discoveryUrl: discoveryUrl, + codeVerifier: result.codeVerifier, + nonce: result.nonce, + scopes: [ + 'openid', + 'email', + ], + ), + ); + + accessToken = tokenResult?.accessToken; + idToken = tokenResult?.idToken; + } else { + final GoogleSignIn googleSignIn = GoogleSignIn( + serverClientId: clientId, scopes: [ 'openid', 'email', ], - ), - ); - - if (result == null) { - throw 'Could not find AuthorizationResponse after authorizing'; + ); + final googleUser = await googleSignIn.signIn(); + final googleAuth = await googleUser!.authentication; + accessToken = googleAuth.accessToken; + idToken = googleAuth.idToken; } - // Request the access and id token to google - final tokenResult = await appAuth.token( - TokenRequest( - clientId, - redirectUrl, - authorizationCode: result.authorizationCode, - discoveryUrl: discoveryUrl, - codeVerifier: result.codeVerifier, - nonce: result.nonce, - scopes: [ - 'openid', - 'email', - ], - ), - ); - - final idToken = tokenResult?.idToken; - if (idToken == null) { - throw 'Could not find idToken from the token response'; + throw 'No ID Token'; + } + if (accessToken == null) { + throw 'No Access Token'; } return supabase.auth.signInWithIdToken( provider: Provider.google, idToken: idToken, - accessToken: tokenResponse?.accessToken, + accessToken: accessToken, nonce: rawNonce, ); } @@ -327,7 +353,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for. - - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. + - For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. You also need to create a web client ID to perform Google sign in on Android. - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. diff --git a/apps/docs/pages/guides/database/extensions/pg_cron.mdx b/apps/docs/pages/guides/database/extensions/pg_cron.mdx index ac35e507c21d6..44cb15b07c3ba 100644 --- a/apps/docs/pages/guides/database/extensions/pg_cron.mdx +++ b/apps/docs/pages/guides/database/extensions/pg_cron.mdx @@ -33,13 +33,8 @@ The `pg_cron` extension is a simple cron-based job scheduler for PostgreSQL that -- Example: enable the "pg_cron" extension create extension pg_cron with schema extensions; --- If you're planning to use a non-superuser role to schedule jobs, --- ensure that they are granted access to the cron schema and its underlying objects beforehand. --- Failure to do so would result in jobs by these roles to not run at all. - -grant usage on schema cron to {{DB user}}; -grant all privileges on all tables in schema cron to {{DB user}}; - +grant usage on schema cron to postgres; +grant all privileges on all tables in schema cron to postgres; -- Example: disable the "pg_cron" extension drop extension if exists pg_cron; diff --git a/apps/docs/pages/guides/database/postgres/configuration.mdx b/apps/docs/pages/guides/database/postgres/configuration.mdx index 0f566e0c0caf2..c18443855411f 100644 --- a/apps/docs/pages/guides/database/postgres/configuration.mdx +++ b/apps/docs/pages/guides/database/postgres/configuration.mdx @@ -12,7 +12,7 @@ Postgres provides a set of sensible defaults for you database size. In some case ## Timeouts -By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage. +By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage. This can be overridden using [Custom Postgres configuration](https://supabase.com/docs/guides/platform/custom-postgres-config#supported-parameters). ### Changing the default timeout diff --git a/apps/docs/pages/guides/database/postgres/roles.mdx b/apps/docs/pages/guides/database/postgres/roles.mdx index 64e85db3e2fef..8a799c16e94eb 100644 --- a/apps/docs/pages/guides/database/postgres/roles.mdx +++ b/apps/docs/pages/guides/database/postgres/roles.mdx @@ -13,10 +13,6 @@ Postgres manages database access permissions using the concept of roles. General In PostgreSQL, roles can function as users or groups of users. Users are roles with login privileges, while groups (also known as role groups) are roles that don't have login privileges but can be used to manage permissions for multiple users. -## Superuser roles - -The superuser role has unrestricted access to the database system. Typically these roles will be able to bypass all security measures (like Row Level Security). Be cautious when granting superuser privileges as it can potentially lead to security risks. - ## Creating roles You can create a role using the `create role` command: diff --git a/apps/docs/pages/guides/database/postgres/row-level-security.mdx b/apps/docs/pages/guides/database/postgres/row-level-security.mdx index 5d767ec2c2718..dde8fb4f8841c 100644 --- a/apps/docs/pages/guides/database/postgres/row-level-security.mdx +++ b/apps/docs/pages/guides/database/postgres/row-level-security.mdx @@ -86,7 +86,7 @@ for select using ( auth.uid() = user_id ); ### INSERT Policies -You can specify select policies with the `with check` clause. +You can specify insert policies with the `with check` clause. Let's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert: @@ -104,13 +104,13 @@ alter table profiles enable row level security; -- 3. Create Policy create policy "Users can create a profile." on profiles for insert -to authenticated -- the Postgres Role (recommended) -using ( auth.id() = user_id ); -- the actual Policy +to authenticated -- the Postgres Role (recommended) +with check ( auth.uid() = user_id ); -- the actual Policy ``` ### UPDATE Policies -You can specify select policies with the `using` clause. +You can specify update policies with the `using` clause. Let's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile: @@ -128,13 +128,13 @@ alter table profiles enable row level security; -- 3. Create Policy create policy "Users can update their own profile." on profiles for update -to authenticated -- the Postgres Role (recommended) -using ( auth.id() = user_id ); -- the actual Policy +to authenticated -- the Postgres Role (recommended) +using ( auth.uid() = user_id ); -- the actual Policy ``` ### DELETE Policies -You can specify select policies with the `using` clause. +You can specify delete policies with the `using` clause. Let's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile: @@ -152,8 +152,8 @@ alter table profiles enable row level security; -- 3. Create Policy create policy "Users can delete a profile." on profiles for delete -to authenticated -- the Postgres Role (recommended) -using ( auth.id() = user_id ); -- the actual Policy +to authenticated -- the Postgres Role (recommended) +using ( auth.uid() = user_id ); -- the actual Policy ``` ## Bypassing Row Level Security diff --git a/apps/docs/pages/guides/functions/secrets.mdx b/apps/docs/pages/guides/functions/secrets.mdx index 9f0b8be850676..8259d4e2d12a3 100644 --- a/apps/docs/pages/guides/functions/secrets.mdx +++ b/apps/docs/pages/guides/functions/secrets.mdx @@ -14,7 +14,7 @@ Deno.env.get(MY_SECRET_NAME) ### Local Development -When developing functions locally, you be able to load environment variables two ways: +When developing functions locally, you will be able to load environment variables in two ways: 1. Through a default `.env` file placed at `supabase/functions/.env`, which will get loaded on `supabase start` 2. Through the `--env-file` option for `supabase functions serve`, for example: `supabase functions serve --env-file ./path/to/.env-file` diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx index b46ffb7f9673d..2c63525d17753 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-angular.mdx @@ -246,7 +246,7 @@ export class AccountComponent implements OnInit { try { this.loading = true const { user } = this.session - let { data: profile, error, status } = await this.supabase.profile(user) + const { data: profile, error, status } = await this.supabase.profile(user) if (error && status !== 406) { throw error diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-expo-react-native.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-expo-react-native.mdx index 846385d21f43e..461a5cd65634c 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-expo-react-native.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-expo-react-native.mdx @@ -276,7 +276,7 @@ export default function Account({ session }: { session: Session }) { setLoading(true) if (!session?.user) throw new Error('No user on the session!') - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', session?.user.id) @@ -320,7 +320,7 @@ export default function Account({ session }: { session: Session }) { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates) + const { error } = await supabase.from('profiles').upsert(updates) if (error) { throw error @@ -501,7 +501,7 @@ export default function Avatar({ url, size = 150, onUpload }: Props) { const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` - let { error } = await supabase.storage.from('avatars').upload(filePath, formData) + const { error } = await supabase.storage.from('avatars').upload(filePath, formData) if (error) { throw error diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx index f381c1ec52910..8ff49b546bc61 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-angular.mdx @@ -256,7 +256,7 @@ export class AccountPage implements OnInit { async getProfile() { try { - let { data: profile, error, status } = await this.supabase.profile + const { data: profile, error, status } = await this.supabase.profile if (error && status !== 406) { throw error } diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx index fbb17ca5464fc..30b63a1b197e7 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-react.mdx @@ -176,7 +176,7 @@ export function AccountPage() { await showLoading(); try { const user = supabase.auth.user(); - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user!.id) @@ -219,7 +219,7 @@ export function AccountPage() { updated_at: new Date(), }; - let { error } = await supabase.from('profiles').upsert(updates, { + const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }); @@ -430,7 +430,7 @@ export function Avatar({ const fileName = `${Math.random()}-${new Date().getTime()}.${ photo.format }`; - let { error: uploadError } = await supabase.storage + const { error: uploadError } = await supabase.storage .from('avatars') .upload(fileName, file); if (uploadError) { diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx index b58717103ae21..41b58ec9290ce 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-ionic-vue.mdx @@ -239,7 +239,7 @@ Let's create a new component for that called `Account.vue`. const toast = await toastController.create({ duration: 5000 }) await loader.present() try { - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) @@ -274,7 +274,7 @@ Let's create a new component for that called `Account.vue`. updated_at: new Date(), } // - let { error } = await supabase.from('profiles').upsert(updates, { + const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }) // @@ -292,7 +292,7 @@ Let's create a new component for that called `Account.vue`. const toast = await toastController.create({ duration: 5000 }) await loader.present() try { - let { error } = await supabase.auth.signOut() + const { error } = await supabase.auth.signOut() if (error) throw error } catch (error: any) { toast.message = error.message @@ -472,7 +472,7 @@ Then create an **AvatarComponent**. .then((blob) => new File([blob], 'my-file', { type: `image/${photo.format}` })) const fileName = `${Math.random()}-${new Date().getTime()}.${photo.format}` - let { error: uploadError } = await supabase.storage + const { error: uploadError } = await supabase.storage .from('avatars') .upload(fileName, file) if (uploadError) { diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx index 4256746a62b85..f658824ecba35 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-nextjs.mdx @@ -437,7 +437,7 @@ export default function AccountForm({ session }) { try { setLoading(true) - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`full_name, username, website, avatar_url`) .eq('id', user?.id) @@ -468,7 +468,7 @@ export default function AccountForm({ session }) { try { setLoading(true) - let { error } = await supabase.from('profiles').upsert({ + const { error } = await supabase.from('profiles').upsert({ id: user?.id, full_name: fullname, username, @@ -563,7 +563,7 @@ export default function AccountForm({ session }: { session: Session | null }) { try { setLoading(true) - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`full_name, username, website, avatar_url`) .eq('id', user?.id) @@ -603,7 +603,7 @@ export default function AccountForm({ session }: { session: Session | null }) { try { setLoading(true) - let { error } = await supabase.from('profiles').upsert({ + const { error } = await supabase.from('profiles').upsert({ id: user?.id as string, full_name: fullname, username, @@ -799,7 +799,7 @@ export default function Avatar({ uid, url, size, onUpload }) { const fileExt = file.name.split('.').pop() const filePath = `${uid}-${Math.random()}.${fileExt}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError @@ -904,7 +904,7 @@ export default function Avatar({ const fileExt = file.name.split('.').pop() const filePath = `${uid}-${Math.random()}.${fileExt}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-nuxt-3.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-nuxt-3.mdx index 1be2aa21b061b..29392ce681d22 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-nuxt-3.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-nuxt-3.mdx @@ -128,7 +128,7 @@ const avatar_path = ref('') loading.value = true const user = useSupabaseUser() -let { data } = await supabase +const { data } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.value.id) @@ -155,7 +155,7 @@ async function updateProfile() { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates, { + const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }) if (error) throw error @@ -169,7 +169,7 @@ async function updateProfile() { async function signOut() { try { loading.value = true - let { error } = await supabase.auth.signOut() + const { error } = await supabase.auth.signOut() if (error) throw error user.value = null } catch (error) { @@ -283,7 +283,7 @@ const uploadAvatar = async (evt) => { const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) throw uploadError @@ -349,7 +349,7 @@ const avatar_path = ref('') loading.value = true const user = useSupabaseUser() -let { data } = await supabase +const { data } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.value.id) @@ -376,7 +376,7 @@ async function updateProfile() { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates, { + const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }) @@ -391,7 +391,7 @@ async function updateProfile() { async function signOut() { try { loading.value = true - let { error } = await supabase.auth.signOut() + const { error } = await supabase.auth.signOut() if (error) throw error } catch (error) { alert(error.message) diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-react.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-react.mdx index 18030dc0b6e6e..6e8fbaf8be974 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-react.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-react.mdx @@ -138,7 +138,7 @@ export default function Account({ session }) { setLoading(true) const { user } = session - let { data, error } = await supabase + const { data, error } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) @@ -172,7 +172,7 @@ export default function Account({ session }) { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates) + const { error } = await supabase.from('profiles').upsert(updates) if (error) { alert(error.message) @@ -316,7 +316,7 @@ export default function Avatar({ url, size, onUpload }) { const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-redwoodjs.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-redwoodjs.mdx index 375198da2f4e6..d1cbff107196e 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-redwoodjs.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-redwoodjs.mdx @@ -320,7 +320,7 @@ const Account = () => { setLoading(true) const user = supabase.auth.user() - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) @@ -355,7 +355,7 @@ const Account = () => { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates, { + const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }) @@ -525,7 +525,7 @@ const Avatar = ({ url, size, onUpload }) => { const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-solidjs.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-solidjs.mdx index 759c23685ce2b..01056870c24ac 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-solidjs.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-solidjs.mdx @@ -146,7 +146,7 @@ const Account: Component = ({ session }) => { setLoading(true) const { user } = session - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) @@ -185,7 +185,7 @@ const Account: Component = ({ session }) => { updated_at: new Date().toISOString(), } - let { error } = await supabase.from('profiles').upsert(updates) + const { error } = await supabase.from('profiles').upsert(updates) if (error) { throw error @@ -336,7 +336,7 @@ const Avatar: Component = (props) => { const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-svelte.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-svelte.mdx index ceb11321c7452..2dcdea50f9339 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-svelte.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-svelte.mdx @@ -173,7 +173,7 @@ Let's create a new component for that called `Account.svelte`. updated_at: new Date().toISOString(), } - let { error } = await supabase.from('profiles').upsert(updates) + const { error } = await supabase.from('profiles').upsert(updates) if (error) { throw error @@ -306,7 +306,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` - let { error } = await supabase.storage.from('avatars').upload(filePath, file) + const { error } = await supabase.storage.from('avatars').upload(filePath, file) if (error) { throw error diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx index b3c30daa25e85..1a453197c5169 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-sveltekit.mdx @@ -163,7 +163,7 @@ Update your `src/routes/+layout.svelte`: export let data - let { supabase, session } = data + const { supabase, session } = data $: ({ supabase, session } = data) onMount(() => { @@ -313,7 +313,7 @@ Create a new `src/routes/account/+page.svelte` file with the content below. export let data export let form - let { session, supabase, profile } = data + const { session, supabase, profile } = data $: ({ session, supabase, profile } = data) let profileForm: HTMLFormElement @@ -517,7 +517,7 @@ Let's create an avatar for the user so that they can upload a profile photo. We const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` - let { error } = await supabase.storage.from('avatars').upload(filePath, file) + const { error } = await supabase.storage.from('avatars').upload(filePath, file) if (error) { throw error diff --git a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx index 849f25e02b5eb..d76df485a6a9b 100644 --- a/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx +++ b/apps/docs/pages/guides/getting-started/tutorials/with-vue-3.mdx @@ -141,7 +141,7 @@ async function getProfile() { loading.value = true const { user } = session.value - let { data, error, status } = await supabase + const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) @@ -174,7 +174,7 @@ async function updateProfile() { updated_at: new Date(), } - let { error } = await supabase.from('profiles').upsert(updates) + const { error } = await supabase.from('profiles').upsert(updates) if (error) throw error } catch (error) { @@ -187,7 +187,7 @@ async function updateProfile() { async function signOut() { try { loading.value = true - let { error } = await supabase.auth.signOut() + const { error } = await supabase.auth.signOut() if (error) throw error } catch (error) { alert(error.message) @@ -313,7 +313,7 @@ const uploadAvatar = async (evt) => { const fileExt = file.name.split('.').pop() const filePath = `${Math.random()}.${fileExt}` - let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) + const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) throw uploadError emit('update:path', filePath) diff --git a/apps/docs/pages/guides/platform/access-control.mdx b/apps/docs/pages/guides/platform/access-control.mdx index 9ec8c23843642..a0e359c4f691b 100644 --- a/apps/docs/pages/guides/platform/access-control.mdx +++ b/apps/docs/pages/guides/platform/access-control.mdx @@ -13,24 +13,27 @@ For each organization, a member can have one of the following roles: - Administrator - Developer -A default organization is created when you first sign in and -you'll be assigned the **Owner** role. -Each member can access all projects under the organization. -Project level invites are not available at this time. -Create a separate organization if you need to restrict access to certain projects. + + +Additional roles (Read-only and Billing) are available on the [team and enterprise plans](https://supabase.com/pricing). + + + +A default organization is created when you first sign in, at which point you'll be assigned the **Owner** role. Keep in mind that members can access all projects within the organization and that project level access control is not available at this time. Create a separate organization if you need to restrict access to certain projects. ## Manage team members -To invite others to collaborate, visit your organization's team settings in the -[Dashboard](https://supabase.com/dashboard/projects) to send an invite link to -another user's email. The invite expires after 24 hours. +To invite others to collaborate, visit your organization's team [settings](/dashboard/org/_/team) to send an invite link to another user's email. The invite expires after 24 hours. -### Transferring ownership of an organization + -Each Supabase organization can have one or more owners. If you no longer want be an owner of an organization, click **Leave team** in the members view (`https://supabase.com/dashboard/org//settings#team`) of your organization. -However, you can only leave an organization when there is _at least one other owner_. +Invites sent from a SAML SSO account can only be accepted by another SAML SSO account from the same identity provider. This is a security measure to prevent accidental invites to accounts not managed by your enterprise's identity provider. + + + +### Transferring ownership of an organization -If you are transferring ownership of your organization to someone else, you will need to invite the new member with the **Owner** role. You can leave the organization after they've accepted the invitation. +Each Supabase organization can have one or more owners. If you no longer want be an owner of an organization, click **Leave team** in your organization's team [settings](/dashboard/org/_/team). You can only leave an organization if there is _at least one other owner_. If you are transferring ownership of your organization to someone else, you will need to invite the new member with the **Owner** role. You can leave the organization after they've accepted the invitation. ### Permissions across roles [#permission-across-roles] diff --git a/apps/docs/pages/guides/platform/compute-add-ons.mdx b/apps/docs/pages/guides/platform/compute-add-ons.mdx index e809e93476a45..646cb64adac41 100644 --- a/apps/docs/pages/guides/platform/compute-add-ons.mdx +++ b/apps/docs/pages/guides/platform/compute-add-ons.mdx @@ -62,6 +62,31 @@ If you need consistent disk performance, choose the 4XL or larger compute add-on If you're unsure of how much throughput or IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://supabase.com/dashboard/project/_/reports). If the `Disk IO % consumed` stat is more than 1%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric goes to 100%, the workload has used up all available disk budget and will revert to baseline performance. Projects that use any disk budget are good candidates for upgrading to a larger compute add-on with higher baseline throughput. +## Postgres Replication Slots and WAL Senders + +[Replication Slots](https://postgresqlco.nf/doc/en/param/max_replication_slots) and [WAL Senders](https://postgresqlco.nf/doc/en/param/max_wal_senders/) are used to enable [Postgres Replication](/docs/guides/database/replication). + +The maximum number of replication slots and WAL senders depends on your compute add-on plan, as follows: + +| Plan | Max Replication Slots | Max WAL Senders | +| ------- | --------------------- | --------------- | +| Starter | 5 | 5 | +| Small | 5 | 5 | +| Medium | 5 | 5 | +| Large | 8 | 8 | +| XL | 24 | 24 | +| 2XL | 80 | 80 | +| 4XL | 80 | 80 | +| 8XL | 80 | 80 | +| 12XL | 80 | 80 | +| 16XL | 80 | 80 | + + + +As mentioned in the Postgres [documentation](https://postgresqlco.nf/doc/en/param/max_replication_slots/), setting `max_replication_slots` to a lower value than the current number of replication slots will prevent the server from starting. If you are downgrading your compute add-on, please ensure that you are using fewer slots than the maximum number of replication slots available for the new compute add-on. + + + export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/platform/oauth-apps/oauth-scopes.mdx b/apps/docs/pages/guides/platform/oauth-apps/oauth-scopes.mdx new file mode 100644 index 0000000000000..484e3cfde590b --- /dev/null +++ b/apps/docs/pages/guides/platform/oauth-apps/oauth-scopes.mdx @@ -0,0 +1,43 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'oauth-scopes', + title: 'Scopes for your OAuth App', + description: 'Scopes let you specify the level of access your integration needs', + subtitle: 'Scopes let you specify the level of access your integration needs', +} + + + +Scopes are only available for OAuth apps. Check out [**our guide**](/docs/guides/platform/oauth-apps/build-a-supabase-integration) to learn how to build an OAuth app integration. + + + +Scopes restrict access to the specific [Supabase Management API endpoints](/docs/reference/api/introduction) for OAuth tokens. All scopes can be specified as read and/or write. + +## Available Scopes + +| Name | Type | Description | +| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Auth` | `Read` | Retrieve a project's auth configuration
Retrieve a project's SAML SSO providers | +| `Auth` | `Write` | Update a project's auth configuration
Create, update, or delete a project's SAML SSO providers | +| `Database` | `Read` | Retrieve the database configuration
Retrieve SQL snippets
Check if the database is in read-only mode
Retrieve a database's SSL enforcement configuration
Retrieve a database's schema typescript types | +| `Database` | `Write` | Create a SQL query
Enable database webhooks on the project
Update the project's database configration
Update a database's SSL enforcement configration
Disable read-only mode for 15mins
Retrieve the pgbouncer configuration
Create a PITR backup for a database | +| `Domains` | `Read` | Retrieve the custom domains for a project
Retrieve the vanity subdomain configuration for a project | +| `Domains` | `Write` | Activate, initialize, reverify, or delete the custom domain for a project
Activate, delete or check the availability of a vanity subdomain for a project | +| `Edge Functions` | `Read` | Retrieve information about a project's edge functions | +| `Edge Functions` | `Write` | Create, update, or delete an edge function | +| `Environment` | `Read` | Retrieve branches in a project | +| `Environment` | `Write` | Create, update, or delete a branch | +| `Organizations` | `Read` | Retrieve an organization's metadata
Retrieve all members in an organization | +| `Organizations` | `Write` | Create an organization | +| `Projects` | `Read` | Retrieve a project's metadata
Check if a project's database is eligible for upgrade
Retrieve a project's network restrictions
Retrieve a project's network bans | +| `Projects` | `Write` | Create a project
Upgrade a project's database
Remove a project's network bans
Update a project's network restrictions | +| `Rest` | `Read` | Retrieve a project's PostgREST configuration | +| `Rest` | `Write` | Update a project's PostgREST configuration | +| `Secrets` | `Read` | Retrieve a project's API keys
Retrieve a project's secrets
Retrieve a project's pgsodium config | +| `Secrets` | `Write` | Create or update a project's secrets
Update a project's pgsodium configuration | + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/platform/performance.mdx b/apps/docs/pages/guides/platform/performance.mdx index e8844a8037546..906618c951324 100644 --- a/apps/docs/pages/guides/platform/performance.mdx +++ b/apps/docs/pages/guides/platform/performance.mdx @@ -203,23 +203,7 @@ Depending on the clients involved, you might be able to configure them to work w ### Allowing higher number of connections -You can configure Postgres by executing the following statement, followed by a server restart: - -```sql -alter system set max_connections = ''; -``` - -Note that the default configuration used by the Supabase platform optimizes the database to maximize resource utilization, and as a result, you might also need to configure other options (e.g. `work_mem`, `shared_buffers`, `maintenance_work_mem`) in order to tune them towards your use-case, and to avoid causing instability in your database. - -Once overridden, the Supabase platform will continue to respect your manually configured value (even if the add-on size is changed), unless the override is removed with the following statement, followed by a server restart: - -```sql -alter system reset max_connections; -alter system reset ; -... -``` - -Configuring the number of PgBouncer connections is not supported at this time. +You can configure Postgres connection limit among other parameters by using [Custom Postgres Config](/docs/guides/platform/custom-postgres-config#custom-postgres-config). export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/realtime.mdx b/apps/docs/pages/guides/realtime.mdx index a043d596c5c9f..590a6c0412ce8 100644 --- a/apps/docs/pages/guides/realtime.mdx +++ b/apps/docs/pages/guides/realtime.mdx @@ -64,7 +64,10 @@ const handleInserts = (payload) => { } // Listen to inserts -const { data: todos, error } = await supabase.from('todos').on('INSERT', handleInserts).subscribe() +supabase + .channel('todos') + .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'todos' }, handleInserts) + .subscribe() ``` Use [subscribe()](/docs/reference/javascript/subscribe) to listen to database changes. diff --git a/apps/docs/pages/guides/realtime/bring-your-own-database.mdx b/apps/docs/pages/guides/realtime/bring-your-own-database.mdx index db805ba7b224f..5e87c26daf4cf 100644 --- a/apps/docs/pages/guides/realtime/bring-your-own-database.mdx +++ b/apps/docs/pages/guides/realtime/bring-your-own-database.mdx @@ -31,6 +31,8 @@ Realtime relies on Postgres' logical replication functionality to get database c - `max_replication_slots`: we recommend `10` because Realtime requires a few slots plus the slots you'll need for your non-Realtime logical replication needs. - `max_slot_wal_keep_size`: we recommend `1024` (MB) so Realtime can attempt to deliver more database changes stored in Postgres. +See [Compute Add-ons](/docs/guides/platform/compute-add-ons#postgres-replication-slots-and-wal-senders) for the recommended `max_replication_slots` at different instance sizes based on the values we use for our own cloud offering. + ## Realtime Database Setup ### `supabase_realtime` Publication diff --git a/apps/docs/pages/guides/realtime/broadcast.mdx b/apps/docs/pages/guides/realtime/broadcast.mdx index 14082d8d19bfa..177ced85477e5 100644 --- a/apps/docs/pages/guides/realtime/broadcast.mdx +++ b/apps/docs/pages/guides/realtime/broadcast.mdx @@ -298,25 +298,51 @@ This is currently available only in the Supabase JavaScript client version 2.37. You can also send a Broadcast message by making an HTTP request to Realtime servers. This is useful when you want to send messages from your server or client without having to first establish a WebSocket connection. + + ```js const channel = client.channel('test-channel') // No need to subscribe to channel channel - .send({ - type: 'broadcast', - event: 'test', - payload: { message: 'Hi' }, - }) - .then((resp) => { - console.log(resp) - }) +.send({ +type: 'broadcast', +event: 'test', +payload: { message: 'Hi' }, +}) +.then((resp) => console.log(resp)) // Remember to clean up the channel client.removeChannel(channel) -``` + +```` + + + +```dart +// No need to subscribe to channel + +channel = client.channel('test-channel'); +final resp = await channel.send( + type: RealtimeListenTypes.broadcast, + payload: { + 'message': 'Hi', + }, + event: "test" +); +print(resp); +```` + + + export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/self-hosting.mdx b/apps/docs/pages/guides/self-hosting.mdx index b8468f8112a26..0262596b24fdd 100644 --- a/apps/docs/pages/guides/self-hosting.mdx +++ b/apps/docs/pages/guides/self-hosting.mdx @@ -97,7 +97,7 @@ export const external = [ { name: 'Digital Ocean', description: 'Deploys using Terraform.', - href: 'https://supabase-on-do.product-docs.pages.dev/developer-center/hosting-supabase-on-digitalocean/', + href: 'https://docs.digitalocean.com/developer-center/hosting-supabase-on-digitalocean/', }, { name: 'StackGres', diff --git a/apps/docs/pages/learn/auth-deep-dive/auth-row-level-security.mdx b/apps/docs/pages/learn/auth-deep-dive/auth-row-level-security.mdx index 81424abf45f39..c19a7476a9c45 100644 --- a/apps/docs/pages/learn/auth-deep-dive/auth-row-level-security.mdx +++ b/apps/docs/pages/learn/auth-deep-dive/auth-row-level-security.mdx @@ -63,10 +63,10 @@ You can see that it's possible to freely read from and write to the table by usi ```js // Writing -let { data, error } = await supabase.from('leaderboard').insert({ name: 'Bob', score: 99999 }) +const { data, error } = await supabase.from('leaderboard').insert({ name: 'Bob', score: 99999 }) // Reading -let { data, error } = await supabase +const { data, error } = await supabase .from('leaderboard') .select('name, score') .order('score', { ascending: false }) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index b8a0b814b8d2f..fa0ff6859fa89 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -43,8 +43,6 @@ Long Hoang Margarita Sandomirskaia Marijana Šimag Mark Burggraf -Michel Pelletier -Mickael Hebert Monica El Khoury Nikita Kotlyarov Oli R @@ -64,6 +62,7 @@ Stojan Dimitrovski Supun Sudaraka Kalidasa Terry Sutton Thor Schaeff +Tyler Fontaine Tyler Shukert TzeYiing L Wen Bo Xie diff --git a/apps/www/_blog/2021-03-11-using-supabase-replit.mdx b/apps/www/_blog/2021-03-11-using-supabase-replit.mdx index 53b83d72c7c94..18b6aeb0973d1 100644 --- a/apps/www/_blog/2021-03-11-using-supabase-replit.mdx +++ b/apps/www/_blog/2021-03-11-using-supabase-replit.mdx @@ -79,7 +79,7 @@ supabase. // or... // async/await syntax const main = async() => { - let { data, error } = supabase + const { data, error } = supabase .from('countries') .select('*') .limit(5) diff --git a/apps/www/_blog/2022-08-09-slack-consolidate-slackbot-to-consolidate-messages.mdx b/apps/www/_blog/2022-08-09-slack-consolidate-slackbot-to-consolidate-messages.mdx index 4f8828c8854dd..fde2537ee28dd 100644 --- a/apps/www/_blog/2022-08-09-slack-consolidate-slackbot-to-consolidate-messages.mdx +++ b/apps/www/_blog/2022-08-09-slack-consolidate-slackbot-to-consolidate-messages.mdx @@ -160,8 +160,8 @@ After right-clicking a message in Slack, you can see the option to select the li Slack links have the following format: - https://ORGANIZATION.slack.com/archives/ - channel_id/pmessage_id + https://ORGANIZATION.slack.com/archives/ + channel_id/pmessage_id
Organization: subdomain used in Slack diff --git a/apps/www/_blog/2023-07-18-flutter-authentication.mdx b/apps/www/_blog/2023-07-18-flutter-authentication.mdx index ddcb61eb074a7..1eb8c67dbf1fe 100644 --- a/apps/www/_blog/2023-07-18-flutter-authentication.mdx +++ b/apps/www/_blog/2023-07-18-flutter-authentication.mdx @@ -52,18 +52,20 @@ flutter create myauthapp then we can install the dependencies. Change the working directory to the newly created app directory and run the following command to install our dependencies. ```dart -flutter pub add supabase_flutter flutter_appauth crypto +flutter pub add supabase_flutter flutter_appauth crypto google_sign_in ``` -We will use [supabase_flutter](https://pub.dev/packages/supabase_flutter) to interact with our Supabase instance. [flutter_appauth](https://pub.dev/packages/flutter_appauth) will be used to implement Google login, and [crypto](https://pub.dev/packages/crypto) is a library that has utility functions for encryption that we will use when performing OIDC logins. +We will use [supabase_flutter](https://pub.dev/packages/supabase_flutter) to interact with our Supabase instance. [google_sign_in](https://pub.dev/packages/google_sign_in) will be used to authenticate on Android, and [flutter_appauth](https://pub.dev/packages/flutter_appauth) will be used for iOS. [crypto](https://pub.dev/packages/crypto) is a library that has utility functions for encryption that we will use when performing OIDC logins. We are done installing our dependencies. Let’s set up [authentication](https://supabase.com/docs/guides/auth) now. ## Configure Google sign-in on Supabase Auth -We will obtain client IDs for iOS and Android from the Google Cloud console, and register them to our Supabase project. +We will obtain client IDs for iOS, Android, and web from the Google Cloud console, and register them to our Supabase project. +We need the web client ID in order to use the Google sign-in flow on Android. -First, create your Google Cloud project [here](https://cloud.google.com/) if you do not have one yet. Within your Google Cloud project, follow the [Configure a Google API Console project for Android](https://developers.google.com/identity/sign-in/android/start-integrating#configure_a_project) guide and [Get an OAuth client ID for the iOS](https://developers.google.com/identity/sign-in/ios/start-integrating#get_an_oauth_client_id) guide to obtain client IDs for Android and iOS respectively. +First, create your Google Cloud project [here](https://cloud.google.com/) if you do not have one yet. +Within your Google Cloud project, follow the [Get an OAuth client ID for the iOS](https://developers.google.com/identity/sign-in/ios/start-integrating#get_an_oauth_client_id) guide, [Configure a Google API Console project for Android](https://developers.google.com/identity/sign-in/android/start-integrating#configure_a_project) guide, and [Get your backend server's OAuth 2.0 client ID](https://developers.google.com/identity/sign-in/android/start-integrating#configure_a_project) to obtain client IDs for iOS, Android, and web respectively. Once you have the client IDs, let’s add them to our Supabase dashboard. If you don’t have a Supabase project created yet, you can create one at [database.new](https://database.new) for free. The name is just an internal name, so we can call it “Auth” for now. Database Password will not be used in this example and can be reconfigured later, so press the `Generate a password` button and let Supabase generate a secure random password. No need to copy it anywhere. The region should be anywhere close to where you live, or where your users live in an actual production app. @@ -71,11 +73,11 @@ Lastly, for the pricing plan choose the free plan that allows you to connect wit ![Supabase project creation](/images/blog/flutter-authentication/supabase-project-creation.png) -Your project should be ready in a minute or two. Once your project is ready, you can open `authentication -> Providers -> Google` to set up Google auth. Toggle the `Enable Sign in with Google` switch first. Then add the two client IDs you obtained in your Google Cloud console to `Authorized Client IDs` field with a comma in between the two client IDs like this: `ANDROID_CLIENT_ID,IOS_CLIENT_ID`. +Your project should be ready in a minute or two. Once your project is ready, you can open `authentication -> Providers -> Google` to set up Google auth. Toggle the `Enable Sign in with Google` switch first. Then add the two client IDs you obtained in your Google Cloud console to `Authorized Client IDs` field with a comma in between the two client IDs like this: `IOS_CLIENT_ID,ANDROID_CLIENT_ID,WEB_CLIENT_ID`. ![Supabase auth Google auth provider](/images/blog/flutter-authentication/supabase-google-provider.png) -We also need some Android specific settings to make [flutter_appauth](https://pub.dev/packages/flutter_appauth#android-setup) work. Open `android/app/build.gradle` and find the `defaultConfig`. We need to set the reversed DNS form of the Android Client ID as the`appAuthRedirectScheme` manifest placeholder value. +We also need some Android specific settings to make [flutter_appauth](https://pub.dev/packages/flutter_appauth#android-setup) not throw during app building. Open `android/app/build.gradle` and find the `defaultConfig`. We need to to provide an empty string here. ```groovy ... @@ -83,15 +85,15 @@ android { ... defaultConfig { ... + // We need an empty manifestPlaceholders for appauth to not throw an error manifestPlaceholders += [ - // *account_id* will be unique for every single app - 'appAuthRedirectScheme': 'com.googleusercontent.apps.*account_id*' + 'appAuthRedirectScheme': '' ] } } ``` -That is it for setting up our [Supabase auth to prepare for Google sign-in](https://supabase.com/docs/guides/auth/social-login/auth-google#using-native-sign-in). +That is it for setting up our [Supabase auth to prepare for Google sign-in](https://supabase.com/docs/guides/auth/social-login/auth-google?platform=flutter). Finally, we can initialize Supabase in our Flutter application with the credentials of our Supabase instance. Update your `main.dart` file and add `Supabase.initialize()` in the `main` function like the following. Note that you will see some errors since the home screen is set to the `LoginScreen`, which we will create later. @@ -145,6 +147,7 @@ import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; import 'package:myauthapp/main.dart'; import 'package:myauthapp/screens/profile_screen.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -182,6 +185,100 @@ class _LoginScreenState extends State { return base64Url.encode(List.generate(16, (_) => random.nextInt(256))); } + Future _googleSignIn() async { + /// TODO: update the iOS and Web client ID with your own. + /// + /// Client ID that you registered with Google Cloud. + /// Note that in order to perform Google sign in on Android, you need to + /// provide the web client ID, not the Android client ID. + final clientId = Platform.isIOS ? 'IOS_CLIENT_ID' : 'WEB_CLIENT_ID'; + + late final String? idToken; + late final String? accessToken; + String? rawNonce; + + // Use AppAuth to perform Google sign in on iOS + // and use GoogleSignIn package for Google sign in on Android + if (Platform.isIOS) { + const appAuth = FlutterAppAuth(); + + // Just a random string + rawNonce = _generateRandomString(); + final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); + + /// Set as reversed DNS form of Google Client ID + `:/` for Google login + final redirectUrl = '${clientId.split('.').reversed.join('.')}:/'; + + /// Fixed value for google login + const discoveryUrl = + 'https://accounts.google.com/.well-known/openid-configuration'; + + // authorize the user by opening the concent page + final result = await appAuth.authorize( + AuthorizationRequest( + clientId, + redirectUrl, + discoveryUrl: discoveryUrl, + nonce: hashedNonce, + scopes: [ + 'openid', + 'email', + 'profile', + ], + ), + ); + + if (result == null) { + throw 'No result'; + } + + // Request the access and id token to google + final tokenResult = await appAuth.token( + TokenRequest( + clientId, + redirectUrl, + authorizationCode: result.authorizationCode, + discoveryUrl: discoveryUrl, + codeVerifier: result.codeVerifier, + nonce: result.nonce, + scopes: [ + 'openid', + 'email', + ], + ), + ); + + accessToken = tokenResult?.accessToken; + idToken = tokenResult?.idToken; + } else { + final GoogleSignIn googleSignIn = GoogleSignIn( + serverClientId: clientId, + scopes: [ + 'openid', + 'email', + ], + ); + final googleUser = await googleSignIn.signIn(); + final googleAuth = await googleUser!.authentication; + accessToken = googleAuth.accessToken; + idToken = googleAuth.idToken; + } + + if (idToken == null) { + throw 'No ID Token'; + } + if (accessToken == null) { + throw 'No Access Token'; + } + + return supabase.auth.signInWithIdToken( + provider: Provider.google, + idToken: idToken, + accessToken: accessToken, + nonce: rawNonce, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -190,75 +287,7 @@ class _LoginScreenState extends State { ), body: Center( child: ElevatedButton( - onPressed: () async { - const appAuth = FlutterAppAuth(); - - // Just a random string - final rawNonce = _generateRandomString(); - final hashedNonce = - sha256.convert(utf8.encode(rawNonce)).toString(); - - /// TODO: update the iOS and Android client ID with your own. - /// - /// Client ID that you registered with Google Cloud. - /// You will have two different values for iOS and Android. - final clientId = - Platform.isIOS ? 'IOS_CLIENT_ID' : 'ANDROID_CLIENT_ID'; - - /// Set as reversed DNS form of Google Client ID + `:/` for Google login - final redirectUrl = '${clientId.split('.').reversed.join('.')}:/'; - - /// Fixed value for google login - const discoveryUrl = - 'https://accounts.google.com/.well-known/openid-configuration'; - - // authorize the user by opening the concent page - final result = await appAuth.authorize( - AuthorizationRequest( - clientId, - redirectUrl, - discoveryUrl: discoveryUrl, - nonce: hashedNonce, - scopes: [ - 'openid', - 'email', - 'profile', - ], - ), - ); - - if (result == null) { - throw 'No result'; - } - - // Request the access and id token to google - final tokenResult = await appAuth.token( - TokenRequest( - clientId, - redirectUrl, - authorizationCode: result.authorizationCode, - discoveryUrl: discoveryUrl, - codeVerifier: result.codeVerifier, - nonce: result.nonce, - scopes: [ - 'openid', - 'email', - ], - ), - ); - - final idToken = tokenResult?.idToken; - - if (idToken == null) { - throw 'No idToken'; - } - - await supabase.auth.signInWithIdToken( - provider: Provider.google, - idToken: idToken, - nonce: rawNonce, - ); - }, + onPressed: _googleSignIn, child: const Text('Google login'), ), ), @@ -272,6 +301,8 @@ In terms of UI, this page is very simple, it just has a basic `Scaffold` with an ![Google sign in](/images/blog/flutter-authentication/google-sign-in.png) Let’s break down what is going on within the `onPressed` callback of the sign in button. +At the top, we check on which platform the user is running the application. +We will break down the iOS flow, because it helps us understand how the Open ID Connect flow works. First, we are generating a [nonce](https://openid.net/specs/openid-connect-core-1_0.html#SelfIssuedDiscovery:~:text=auth_time%20response%20parameter.)-,nonce,-String%20value%20used), which is essentially just a random string. This string is later passed to Google after being hashed to verify that the ID token has not been tampered to prevent a man-in-the-middle attack. @@ -349,6 +380,8 @@ await supabase.auth.signInWithIdToken( ); ``` +The android flow goes through similar steps, but all of the heavy lifting of obtaining the access token and ID token is done by the `google_sign_in` package. + ## Create the Profile Screen The `ProfileScreen` will be just a simple UI presenting some of the information we obtained in the `LoginPage`. We can access the user data with `supabase.auth.currentUser`, where Supabase has saved the personal information in a property called `userMetadata`. In this example, we are displaying the `avatar_url` and `full_name` to display a basic profile page. Create a `lib/screens/profile_screen.dart` file and add the following. diff --git a/apps/www/components/AuthWidget/AuthWidgetSection.tsx b/apps/www/components/AuthWidget/AuthWidgetSection.tsx index 406247fe73cfa..7ddc555e569f6 100644 --- a/apps/www/components/AuthWidget/AuthWidgetSection.tsx +++ b/apps/www/components/AuthWidget/AuthWidgetSection.tsx @@ -55,13 +55,13 @@ function AuthWidgetSection() { return (
-
+
-
+
-

Acme Industries

+

Acme Industries

-

- Sign in today for Supa stuff -

+

Sign in today for Supa stuff

@@ -116,7 +114,7 @@ function AuthWidgetSection() {

Auth UI

Pre-built auth widgets to get started in minutes.

-

+

Customizable authentication UI component with custom themes and extensible styles to match your brand and aesthetic

@@ -135,7 +133,7 @@ function AuthWidgetSection() {
- +
@@ -163,7 +161,7 @@ function AuthWidgetSection() { className={[ 'h-10 w-10 rounded-full border-2 border-orange-900 bg-orange-300 transition hover:scale-105 ', brandColor.brand === 'var(--colors-orange9)' - ? ' ring-scale-400 border-scale-800 !bg-orange-900 ring-2 drop-shadow-lg dark:ring-white' + ? ' ring-foreground-muted border-foreground-lighter !bg-orange-900 ring-2 drop-shadow-lg' : '', ].join(' ')} > @@ -178,7 +176,7 @@ function AuthWidgetSection() { className={[ 'border-crimson-900 bg-crimson-300 h-10 w-10 rounded-full border-2 transition hover:scale-105 ', brandColor.brand === 'var(--colors-crimson9)' - ? ' ring-scale-400 border-scale-800 !bg-crimson-900 ring-2 drop-shadow-lg dark:ring-white' + ? ' ring-foreground-muted border-foreground-lighter !bg-crimson-900 ring-2 drop-shadow-lg' : '', ].join(' ')} > @@ -193,7 +191,7 @@ function AuthWidgetSection() { className={[ 'h-10 w-10 rounded-full border-2 border-indigo-900 bg-indigo-300 transition hover:scale-105 ', brandColor.brand === 'var(--colors-indigo9)' - ? ' ring-scale-400 border-scale-800 !bg-indigo-900 ring-2 drop-shadow-lg dark:ring-white' + ? ' ring-foreground-muted border-foreground-lighter !bg-indigo-900 ring-2 drop-shadow-lg dark:ring-white' : '', ].join(' ')} > @@ -201,14 +199,14 @@ function AuthWidgetSection() {
- +
- +
{' '}
diff --git a/apps/www/components/Avatar.tsx b/apps/www/components/Avatar.tsx index 84db7935cedeb..bc9a07ab5116c 100644 --- a/apps/www/components/Avatar.tsx +++ b/apps/www/components/Avatar.tsx @@ -14,7 +14,7 @@ export default function Avatar(props: Props) { style={{ margin: 0 }} alt={`${caption} avatar`} /> -
+

{caption}

diff --git a/apps/www/components/Blog/BlogFilters.tsx b/apps/www/components/Blog/BlogFilters.tsx index b222cca3ddef0..8440084b48b12 100644 --- a/apps/www/components/Blog/BlogFilters.tsx +++ b/apps/www/components/Blog/BlogFilters.tsx @@ -167,7 +167,7 @@ const BlogFilters = ({ posts, setPosts, setCategory, allCategories, handlePosts setSearchKey('') setShowSearchInput(false) }} - className="text-scale-1100 hover:text-scale-1200" + className="text-light hover:text-foreground" > diff --git a/apps/www/components/Blog/BlogListItem.tsx b/apps/www/components/Blog/BlogListItem.tsx index ddecbec5552d7..e00a71e08de6c 100644 --- a/apps/www/components/Blog/BlogListItem.tsx +++ b/apps/www/components/Blog/BlogListItem.tsx @@ -47,10 +47,10 @@ const BlogListItem = ({ post }: Props) => { />
-

{post.title}

-

{post.description}

+

{post.title}

+

{post.description}

{post.date && ( -
+

{post.date}

{post.readingTime && ( <> @@ -64,7 +64,7 @@ const BlogListItem = ({ post }: Props) => {
{author.map((author: any, i: number) => { return ( -
+
{author.author_image_url && ( (
@@ -6,19 +7,8 @@ const ShareArticleActions = ({ title, slug }: { title: string; slug: string }) = passHref href={`https://twitter.com/share?text=${title}&url=https://supabase.com/blog/${slug}`} > - - - - + + @@ -26,37 +16,16 @@ const ShareArticleActions = ({ title, slug }: { title: string; slug: string }) = passHref href={`https://www.linkedin.com/shareArticle?url=https://supabase.com/blog/${slug}&title=${title}`} > - - - - + + - - - - + +
diff --git a/apps/www/components/CTABanner/index.tsx b/apps/www/components/CTABanner/index.tsx index ff1f2ddec1a8d..11aaa1a6510c5 100644 --- a/apps/www/components/CTABanner/index.tsx +++ b/apps/www/components/CTABanner/index.tsx @@ -9,15 +9,15 @@ const CTABanner = ({ darkerBg, className }: Props) => { return (

- Build in a weekend, - + Build in a weekend, + {' '} scale to millions diff --git a/apps/www/components/Carousels/ImageCarousel.tsx b/apps/www/components/Carousels/ImageCarousel.tsx index 802a1bf4ff5e2..f689df842a9f1 100644 --- a/apps/www/components/Carousels/ImageCarousel.tsx +++ b/apps/www/components/Carousels/ImageCarousel.tsx @@ -170,7 +170,7 @@ function ImageCarousel(props: ImageCarouselProps) { {props.content.map((content, i) => { return ( -

{content.title}

+

{content.title}

{content.text}

- + {extension.detail_title}

diff --git a/apps/www/components/CodeBlock/CodeBlock.tsx b/apps/www/components/CodeBlock/CodeBlock.tsx index 1269b1f11ffe0..52d2596aec36d 100644 --- a/apps/www/components/CodeBlock/CodeBlock.tsx +++ b/apps/www/components/CodeBlock/CodeBlock.tsx @@ -63,8 +63,8 @@ function CodeBlock(props: CodeBlockProps) { {filename && (

{props.title}

{props.description}

-
+
{props.author
- {props.author} + {props.author}
@@ -37,9 +36,8 @@ function ExampleCard(props: any) {
{props.repo_name} diff --git a/apps/www/components/FeatureColumn.tsx b/apps/www/components/FeatureColumn.tsx index e7607c9d25397..56ea60024cd6e 100644 --- a/apps/www/components/FeatureColumn.tsx +++ b/apps/www/components/FeatureColumn.tsx @@ -2,7 +2,7 @@ function FeatureColumn({ icon, title, text }: any) { return ( <> {icon &&
{icon}
} -

{title}

+

{title}

{text}

) diff --git a/apps/www/components/Footer/index.tsx b/apps/www/components/Footer/index.tsx index 4c9bd8fe54b54..f383b6b4e69b3 100644 --- a/apps/www/components/Footer/index.tsx +++ b/apps/www/components/Footer/index.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import { useTheme } from 'next-themes' -import { Badge, cn } from 'ui' +import { Badge, IconDiscord, IconGitHubSolid, IconTwitterX, IconYoutubeSolid, cn } from 'ui' import Image from 'next/image' import { useRouter } from 'next/router' import ThemeToggle from '@ui/components/ThemeProvider/ThemeToggle' @@ -72,9 +72,7 @@ const Footer = (props: Props) => { className="text-lighter hover:text-foreground transition" > Twitter - + { className="text-lighter hover:text-foreground transition" > GitHub - + { className="text-lighter hover:text-foreground transition" > Discord - + { className="text-lighter hover:text-foreground transition" > Youtube - +
diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index f67ade341c9f5..65c438db4e3b4 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -33,7 +33,7 @@ const Hero = () => { {/*
*/} -

+

Build in a weekend @@ -41,7 +41,7 @@ const Hero = () => { Scale to millions

-

+

Supabase is an open source Firebase alternative.{' '}
Start your project with a Postgres database, Authentication, instant APIs, Edge diff --git a/apps/www/components/Hero/HeroFrameworks.tsx b/apps/www/components/Hero/HeroFrameworks.tsx index 08d8904a40ec0..ab0dcd291fb58 100644 --- a/apps/www/components/Hero/HeroFrameworks.tsx +++ b/apps/www/components/Hero/HeroFrameworks.tsx @@ -64,7 +64,7 @@ const HeroFrameworks = ({ className }: { className?: string }) => { return (

- Works seamlessly with 20+ frameworks + Works seamlessly with 20+ frameworks
{frameworks.map((framework) => ( diff --git a/apps/www/components/InteractiveShimmerCard/index.tsx b/apps/www/components/InteractiveShimmerCard/index.tsx index 0d371181cfc4c..c28b9b72edc72 100644 --- a/apps/www/components/InteractiveShimmerCard/index.tsx +++ b/apps/www/components/InteractiveShimmerCard/index.tsx @@ -81,7 +81,7 @@ const InteractiveShimmerCard = ({ >
diff --git a/apps/www/components/LaunchWeek/5/ScheduleInfo.tsx b/apps/www/components/LaunchWeek/5/ScheduleInfo.tsx index 56fb9a1557d70..f9bb8d6b01fad 100644 --- a/apps/www/components/LaunchWeek/5/ScheduleInfo.tsx +++ b/apps/www/components/LaunchWeek/5/ScheduleInfo.tsx @@ -4,19 +4,19 @@ import Link from 'next/link' export function ScheduleInfo() { return (
-
-

Week Schedule

+
+

Week Schedule

Each day of the week we will announce a new item, every day, from Monday to Friday.

-

+

The first launch will be on Monday 08:00 PT | 11:00 ET.

-

You can still win a lucky gold ticket

-

+

You can still win a lucky gold ticket

+

A few of the lucky attendees for Launch Week will get a limited edition Supabase goodie bag.

diff --git a/apps/www/components/LaunchWeek/6/HackathonSection.tsx b/apps/www/components/LaunchWeek/6/HackathonSection.tsx index c2be728bb50af..2e347aedc3301 100644 --- a/apps/www/components/LaunchWeek/6/HackathonSection.tsx +++ b/apps/www/components/LaunchWeek/6/HackathonSection.tsx @@ -9,13 +9,11 @@ export default function LaunchHero() {
-

Launch Week Hackathon

+

Launch Week Hackathon

Closed
-

- Submissions close Sunday 21st Aug 23:59 (PT). -

+

Submissions close Sunday 21st Aug 23:59 (PT).

@@ -34,22 +32,22 @@ export default function LaunchHero() {
-

Prizes

-

+

Prizes

+

There are 5 categories to win. There will be a prize for the winner and a runner-up prize in each category.

-

Submission

-

+

Submission

+

Submit your project through{' '} madewithsupabase.com .

-

+

All submissions must be open source and publically available.

diff --git a/apps/www/components/LaunchWeek/6/LaunchHero.tsx b/apps/www/components/LaunchWeek/6/LaunchHero.tsx index 1974f7c782208..906880083b5ba 100644 --- a/apps/www/components/LaunchWeek/6/LaunchHero.tsx +++ b/apps/www/components/LaunchWeek/6/LaunchHero.tsx @@ -6,10 +6,10 @@ export default function LaunchHero() { return (
-

+

{Controller.hero_header}

-

+

Stay tuned all week for daily announcements

diff --git a/apps/www/components/LaunchWeek/6/PreLaunchTeaser.tsx b/apps/www/components/LaunchWeek/6/PreLaunchTeaser.tsx index e49ee5a089a12..79b4c658e9ea1 100644 --- a/apps/www/components/LaunchWeek/6/PreLaunchTeaser.tsx +++ b/apps/www/components/LaunchWeek/6/PreLaunchTeaser.tsx @@ -68,8 +68,8 @@ export function PreLaunchTeaser() { })}
-

Founders Fireside Chat

-

+

Founders Fireside Chat

+

Our two co-founders, Copple and Ant, discuss open source development and the future of Supabase.

@@ -77,12 +77,12 @@ export function PreLaunchTeaser() {
-
+
-

Supabase Series B

-

+

Supabase Series B

+

Supabase raised $80M in May, bringing our total funding to $116M.

diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButton.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButton.tsx index 364e9ccd5ebf0..a3db8d3879ef7 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButton.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButton.tsx @@ -6,10 +6,9 @@ const ArticleButton = (props: Article) => {
{props.title} - {props.description} + {props.description}
diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx index 8aee78e4070e4..a0a466a13c2e9 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ArticleButtonListItem.tsx @@ -9,10 +9,10 @@ const ArticleButtonListItem = (props: Article) => {
- + {props.title} -

{props.description}

+

{props.description}

diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx index 14fa872e6742a..9b9c200fbd7f7 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButton.tsx @@ -16,7 +16,7 @@ const ProductButton = (props: Article) => {
{props.title} - {props.description} + {props.description}
diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButtonListItem.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButtonListItem.tsx index 1aaafcdd21b96..f33913806e5da 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/ProductButtonListItem.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/ProductButtonListItem.tsx @@ -19,7 +19,7 @@ export const ProductButtonListItem = (props: Article) => {
{props.title} -

{props.description}

+

{props.description}

diff --git a/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx b/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx index db093a84ca839..c26c4f716c6a2 100644 --- a/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx +++ b/apps/www/components/LaunchWeek/7/LaunchSection/index.tsx @@ -30,7 +30,7 @@ export const LaunchSection = (props: WeekDayProps) => { >
{/* END timeline dot */}
- {props.date} + {props.date} {props.shipped ? (
@@ -44,11 +44,11 @@ export const LaunchSection = (props: WeekDayProps) => { Not shipped yet )}
-

+

{props.shipped ? props.title : props.dd + ' 08:00 PT | 11:00 ET'}

- {props.shipped &&

{props.description}

} + {props.shipped &&

{props.description}

}
) @@ -104,7 +104,7 @@ export const LaunchSection = (props: WeekDayProps) => { >
@@ -116,9 +116,9 @@ export const LaunchSection = (props: WeekDayProps) => { hideFooter header={
- {props.title} + {props.title} setVideoVisible(false)} />
@@ -179,7 +179,7 @@ export const LaunchSection = (props: WeekDayProps) => {
-

New releases

+

New releases

{article.products && article.products.map((product: Product, index) => ( diff --git a/apps/www/components/LaunchWeek/7/Releases/components/index.tsx b/apps/www/components/LaunchWeek/7/Releases/components/index.tsx index 365347c45c937..999f47637d180 100644 --- a/apps/www/components/LaunchWeek/7/Releases/components/index.tsx +++ b/apps/www/components/LaunchWeek/7/Releases/components/index.tsx @@ -144,7 +144,7 @@ export const AccordionHeader = ({ date, day, title, shipped }: any) => { - + {day} {date && ( @@ -153,7 +153,7 @@ export const AccordionHeader = ({ date, day, title, shipped }: any) => { )}
- {shipped && {title}} + {shipped && {title}}
) } @@ -161,7 +161,7 @@ export const MultistepSectionHeader = ({ title, blog }: any) => { return (
- {title && {title}} + {title && {title}} {!!blog && ( Blog post diff --git a/apps/www/components/LaunchWeek/7/Ticket/ActualTicket.tsx b/apps/www/components/LaunchWeek/7/Ticket/ActualTicket.tsx index 72afd641dfbd4..7c9b016129bac 100644 --- a/apps/www/components/LaunchWeek/7/Ticket/ActualTicket.tsx +++ b/apps/www/components/LaunchWeek/7/Ticket/ActualTicket.tsx @@ -83,7 +83,7 @@ export default function Ticket({ isMobile && styles['ticket-hero'], ].join(' ')} > -
+

-
+
Connect with GitHub @@ -79,7 +79,7 @@ export default function TicketActions({ }`} > {userData.sharedOnTwitter && ( -
+
)} @@ -100,7 +100,7 @@ export default function TicketActions({ }`} > {userData.sharedOnLinkedIn && ( -
+
)} diff --git a/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx b/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx index a0e7deeb830b8..9bcbfe22ec530 100644 --- a/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx +++ b/apps/www/components/LaunchWeek/7/Ticket/TicketForm.tsx @@ -153,7 +153,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt htmlType="submit" disabled={formState === 'loading' || Boolean(session)} > - + {session ? ( <> @@ -169,7 +169,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt {session ? : null}
- {/* {!session &&

Only public info will be used.

} */} + {/* {!session &&

Only public info will be used.

} */}
) diff --git a/apps/www/components/LaunchWeek/7/Ticket/form.tsx b/apps/www/components/LaunchWeek/7/Ticket/form.tsx index 15b20e2edb8a0..3e0000bf795d9 100644 --- a/apps/www/components/LaunchWeek/7/Ticket/form.tsx +++ b/apps/www/components/LaunchWeek/7/Ticket/form.tsx @@ -129,7 +129,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) { align === 'Left' ? 'text-center xl:text-left' : 'text-center' )} > -

+

Register to get your ticket and stay tuned all week for daily announcements

@@ -162,7 +162,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) { transition-all border border-scale-300 bg-scaleA-200 h-10 focus:border-scale-500 focus:ring-scaleA-300 - text-scale-1200 text-base rounded-full w-full px-5 + text-foreground text-base rounded-full w-full px-5 `} type="email" autoComplete="email" @@ -179,7 +179,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) { type="submit" className={[ 'transition-all', - 'absolute bg-scale-300 text-scale-1200 border border-scale-600 text-sm hover:bg-scale-400', + 'absolute bg-scale-300 text-foreground border border-scale-600 text-sm hover:bg-scale-400', 'rounded-full px-4', 'focus:invalid:border-scale-500 focus:invalid:ring-scaleA-300', 'absolute right-1 my-auto h-8 top-0 bottom-0', diff --git a/apps/www/components/LaunchWeek/8/LW8Meetups.tsx b/apps/www/components/LaunchWeek/8/LW8Meetups.tsx index 1d5fb14403cd7..07501cdb59ac5 100644 --- a/apps/www/components/LaunchWeek/8/LW8Meetups.tsx +++ b/apps/www/components/LaunchWeek/8/LW8Meetups.tsx @@ -64,8 +64,8 @@ const LW8Meetups = ({ meetups }: { meetups?: Meetup[] }) => { target="_blank" className={[ 'w-full group py-0 flex items-center gap-2 md:gap-4 text-lg sm:text-2xl xl:text-4xl border-b border-[#111718]', - 'hover:text-scale-1200', - isLive ? 'text-scale-1100' : 'text-[#56646B]', + 'hover:text-foreground', + isLive ? 'text-light' : 'text-[#56646B]', !link && 'pointer-events-none', ].join(' ')} > diff --git a/apps/www/components/LaunchWeek/8/LabelBadge.tsx b/apps/www/components/LaunchWeek/8/LabelBadge.tsx index 521970158faac..d50b101322760 100644 --- a/apps/www/components/LaunchWeek/8/LabelBadge.tsx +++ b/apps/www/components/LaunchWeek/8/LabelBadge.tsx @@ -8,7 +8,7 @@ interface Props { export default function LabelBadge({ text, className }: Props) { return ( - {text} + {text} ) } diff --git a/apps/www/components/LaunchWeek/8/Releases/components/index.tsx b/apps/www/components/LaunchWeek/8/Releases/components/index.tsx index 36e7b188038fb..14158d2963cc7 100644 --- a/apps/www/components/LaunchWeek/8/Releases/components/index.tsx +++ b/apps/www/components/LaunchWeek/8/Releases/components/index.tsx @@ -197,7 +197,7 @@ export const AccordionHeader = ({ }) => (
( -
+
@@ -291,7 +291,7 @@ export const MultistepSectionHeader = ({ title, blog }: any) => { return (
- {title && {title}} + {title && {title}} {!!blog && ( Blog post diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx index 914e8ee864ed9..b59d219ffea66 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketActions.tsx @@ -84,7 +84,7 @@ export default function TicketActions({ ].join(' ')} > {userData.sharedOnTwitter && ( -
+
)} @@ -98,7 +98,7 @@ export default function TicketActions({ ].join(' ')} > {userData.sharedOnLinkedIn && ( -
+
)} diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketContainer.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketContainer.tsx index 7416c69bb093a..33ca484682d23 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketContainer.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketContainer.tsx @@ -66,7 +66,7 @@ export default function TicketContainer({ user, sharePage, referrals, supabase } isMobile && styles['ticket-hero'], ].join(' ')} > -
+

{!sharePage ? ( name ? ( @@ -104,7 +104,7 @@ export default function TicketContainer({ user, sharePage, referrals, supabase } )}

-
+
{!sharePage ? ( golden ? (

@@ -115,7 +115,7 @@ export default function TicketContainer({ user, sharePage, referrals, supabase }

Customize your ticket and boost your chances of winning{' '} - limited edition awards + limited edition awards {' '} by sharing it with the community.

@@ -125,7 +125,7 @@ export default function TicketContainer({ user, sharePage, referrals, supabase }

Generate and share your own custom ticket for a chance to win{' '} - awesome swag + awesome swag .

diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx index 6d6f848b5365a..4c1058671f758 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketCustomizationForm.tsx @@ -50,9 +50,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => {
debouncedChangeHandler()}>
{!IS_SAVED && !HAS_ERROR && ( - - Connected account - + Connected account )} {IS_SAVED && Saved} {HAS_ERROR && ( @@ -63,7 +61,7 @@ const TicketCustomizationForm = ({ supabase, user }: Props) => { @{user.username}
{ icon={} /> { } /> { } /> +
{LW8_DATE} supabase.com/launch-week
diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketForm.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketForm.tsx index 51a1b7bcb416d..66b8dd0683b9a 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketForm.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketForm.tsx @@ -146,7 +146,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt htmlType="submit" disabled={formState === 'loading' || Boolean(session)} > - + {session ? ( <> @@ -162,7 +162,7 @@ export default function TicketForm({ defaultUsername = '', setTicketGenerationSt {session ? : null}
- {/* {!session &&

Only public info will be used.

} */} + {/* {!session &&

Only public info will be used.

} */}
) diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketNumber.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketNumber.tsx index d7348a6b545c3..5d160eae45faa 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketNumber.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketNumber.tsx @@ -18,7 +18,7 @@ export default function TicketNumber({ number, golden = false }: Props) { ` md:absolute text-[16px] md:text-[22px] w-full px-2 py-8 md:w-[max-content] leading-[1] md:transform md:-rotate-90 md:origin-center - text-scale-1100 text-center font-mono tracking-[0.8rem] + text-light text-center font-mono tracking-[0.8rem] `, golden ? styles['ticket-number-gold'] : styles['ticket-number'], ].join(' ')} diff --git a/apps/www/components/LaunchWeek/8/Ticket/TicketProfile.tsx b/apps/www/components/LaunchWeek/8/Ticket/TicketProfile.tsx index e1cacdff17188..49c83e67cd1e5 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/TicketProfile.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/TicketProfile.tsx @@ -18,7 +18,7 @@ export default function TicketProfile({ user, ticketGenerationState, golden = fa return (
-
+

{name || username || 'Your Name'}

{HAS_NO_META && username &&

@{username}

}
diff --git a/apps/www/components/LaunchWeek/8/Ticket/ticket-copy.tsx b/apps/www/components/LaunchWeek/8/Ticket/ticket-copy.tsx index 65e700c673019..13f4c9b2f86b9 100644 --- a/apps/www/components/LaunchWeek/8/Ticket/ticket-copy.tsx +++ b/apps/www/components/LaunchWeek/8/Ticket/ticket-copy.tsx @@ -39,7 +39,7 @@ export default function TicketCopy({ username, isGolden }: Props) { }, 2000) }) }} - className="h-full flex items-center gap-3 w-full truncate relative pr-20 text-scale-1100 hover:text-scale-1200" + className="h-full flex items-center gap-3 w-full truncate relative pr-20 text-light hover:text-foreground" >

{ return ( <> {!hideHeader &&