diff --git a/DEVELOPERS.md b/DEVELOPERS.md index c9e45c9d5e9..585e7359e3d 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -71,11 +71,11 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab Then visit, and edit, any of the following sites: -| Site | Directory | Scope name | Description | Local development server | -| -------------------------------------------- | -------------- | ---------- | ------------------------------------ | ------------------------ | -| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 | -| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 | -| [supabase.com/docs] Next.js based Docs site | `/apps/docs` . | docs | Guides and Reference (Next.js based) | http://localhost:3001 | +| Site | Directory | Scope name | Description | Local development server | +| ---------------------------------------------- | -------------- | ---------- | ------------------------------------ | ------------------------ | +| [supabase.com](https://supabase.com) | `/apps/www` | www | The main website | http://localhost:3000 | +| [app.supabase.com](https://app.supabase.com) | `/studio` | studio | Studio dashboard | http://localhost:8082 | +| [supabase.com/docs](https://supabase.com/docs) | `/apps/docs` | docs | Guides and Reference (Next.js based) | http://localhost:3001 | #### Running sites individually diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index e1bc479e468..305ac4fa170 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -3,9 +3,9 @@ export const getAnchor = (text: any): string | undefined => { if (typeof text === 'object') { if (Array.isArray(text)) { const customAnchor = text.find( - (x) => typeof x === 'string' && x.includes('{#') && x.endsWith('}') + (x) => typeof x === 'string' && x.includes('[#') && x.endsWith(']') ) - if (customAnchor !== undefined) return customAnchor.slice(3, customAnchor.indexOf('}')) + if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']')) const formattedText = text .map((x) => { @@ -33,8 +33,8 @@ export const getAnchor = (text: any): string | undefined => { return anchor } } else if (typeof text === 'string') { - if (text.includes('{#') && text.endsWith('}')) { - return text.slice(text.indexOf('{#') + 2, text.indexOf('}')) + if (text.includes('[#') && text.endsWith(']')) { + return text.slice(text.indexOf('[#') + 2, text.indexOf(']')) } else { return text .toLowerCase() @@ -48,9 +48,9 @@ export const getAnchor = (text: any): string | undefined => { export const removeAnchor = (text: any) => { if (typeof text === 'object' && Array.isArray(text)) { - return text.filter((x) => !(typeof x === 'string' && x.includes('{#') && x.endsWith('}'))) + return text.filter((x) => !(typeof x === 'string' && x.includes('[#') && x.endsWith(']'))) } else if (typeof text === 'string') { - if (text.indexOf('{#') > 0) return text.slice(0, text.indexOf('{#')) + if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#')) else return text } return text diff --git a/apps/docs/components/CustomHTMLElements/Heading.tsx b/apps/docs/components/CustomHTMLElements/Heading.tsx index 98703bbd944..b07ea6cab17 100644 --- a/apps/docs/components/CustomHTMLElements/Heading.tsx +++ b/apps/docs/components/CustomHTMLElements/Heading.tsx @@ -8,7 +8,6 @@ import { useInView } from 'react-intersection-observer' const Heading = ({ tag, children }) => { const HeadingTag = `${tag}` as any - const anchor = getAnchor(children) const link = `#${anchor}` diff --git a/apps/docs/docs/reference/api/release-notes.mdx b/apps/docs/docs/reference/api/release-notes.mdx index 266d24b2f35..9cc40bf694e 100644 --- a/apps/docs/docs/reference/api/release-notes.mdx +++ b/apps/docs/docs/reference/api/release-notes.mdx @@ -3,7 +3,7 @@ id: release-notes title: Release Notes --- -## v1.0.0 {#1.0.0} +## v1.0.0 [#1.0.0] Adds the following routes: diff --git a/apps/docs/docs/reference/javascript/installing.mdx b/apps/docs/docs/reference/javascript/installing.mdx index 8fdc1a90e8a..b5be32e09a4 100644 --- a/apps/docs/docs/reference/javascript/installing.mdx +++ b/apps/docs/docs/reference/javascript/installing.mdx @@ -8,30 +8,42 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem' -All JavaScript libraries are built directly by the Supabase team. this is in reference/js/installing - -Other languages are built by the community and supported by Supabase. - ## JavaScript -Via NPM + + -```bash +```sh npm install @supabase/supabase-js ``` -Via Yarn + + -```bash +```sh yarn add @supabase/supabase-js ``` -Find the source code on [GitHub](https://github.com/supabase/supabase-js). + + -Or via CDN +### CDN ```js //or ``` + +## Deno + +You can use supabase-js in the Deno runtime via esm.sh: + +```ts +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +``` diff --git a/apps/docs/generator/templates/ApiTemplate.ts b/apps/docs/generator/templates/ApiTemplate.ts index e4b7c565e93..bf95eef4f0c 100644 --- a/apps/docs/generator/templates/ApiTemplate.ts +++ b/apps/docs/generator/templates/ApiTemplate.ts @@ -16,7 +16,7 @@ toc_max_heading_level: 3 <% sections.forEach(function(section){ %> -## <%- section.title %> {#<%= section.id %>} +## <%- section.title %> [#<%= section.id %>] <%- section.description %> @@ -24,7 +24,7 @@ toc_max_heading_level: 3 -### <%- operation.summary %> {#<%- operation.operationId %>} +### <%- operation.summary %> [#<%- operation.operationId %>] \`\`\` <%- operation.operation.toUpperCase() %> <%- operation.fullPath %> diff --git a/apps/docs/generator/templates/ConfigTemplate.ts b/apps/docs/generator/templates/ConfigTemplate.ts index 5958218077f..3300e030de8 100644 --- a/apps/docs/generator/templates/ConfigTemplate.ts +++ b/apps/docs/generator/templates/ConfigTemplate.ts @@ -14,11 +14,11 @@ toc_max_heading_level: 3 -## <%= section.title %> {#<%= section.id %>} +## <%= section.title %> [#<%= section.id %>] <% section.parameters.forEach(function(parameter){ %> -### \`<%- parameter.title %>\` {#<%= parameter.id %>} +### \`<%- parameter.title %>\` [#<%= parameter.id %>] <%- parameter.description %> diff --git a/apps/docs/pages/guides/api.mdx b/apps/docs/pages/guides/api.mdx index 07f55536c1a..ac135dbc7b4 100644 --- a/apps/docs/pages/guides/api.mdx +++ b/apps/docs/pages/guides/api.mdx @@ -21,7 +21,7 @@ The APIs are: - **Fast.**
Our benchmarks for basic reads are more than 300% faster than Firebase. The API is a very thin layer on top of Postgres, which does most of the heavy lifting. - **Scalable.**
The API can serve thousands of simultaneous requests, and works well for Serverless workloads. -## REST API {#rest-api-overview} +## REST API [#rest-api-overview] Supabase provides a RESTful API using [PostgREST](https://postgrest.org/). This is a very thin API layer on top of Postgres. It provides everything you need from a CRUD API: @@ -41,7 +41,7 @@ It provides everything you need from a CRUD API: > -## GraphQL API {#graphql-api-overview} +## GraphQL API [#graphql-api-overview] @@ -51,7 +51,7 @@ GraphQL is in Beta, and may have breaking changes. It is only available on self- GraphQL in Supabase works through [pg_graphql](https://supabase.com/blog/pg-graphql), an open source PostgreSQL extension for GraphQL. -## Realtime API {#realtime-api-overview} +## Realtime API [#realtime-api-overview] Supabase provides a Realtime API using [Realtime](https://github.com/supabase/realtime). You can use this to listen to database changes over websockets. Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications. @@ -130,7 +130,7 @@ You are provided with two keys: ### Accessing the docs in the Dashboard -#### REST API {#rest-api-dashboard-docs} +#### REST API [#rest-api-dashboard-docs] Supabase generates documentation in the [Dashboard](https://app.supabase.com) which updates as you make database changes. Let's view the documentation for a `countries` table which we created in our database. diff --git a/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx b/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx index 2b4eff8c77a..2a7814999e3 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/auth-ui.mdx @@ -147,7 +147,7 @@ const App = () => ( If you created your own theme, you may not need to override any of the them. -### Create your own theme {#create-theme} +### Create your own theme [#create-theme] You can create your own theme by following the same structure within a `appearance.theme` property. See the list of [tokens within a theme](https://github.com/supabase-community/auth-ui/blob/main/packages/react/common/theming/Themes.tsx). @@ -199,7 +199,7 @@ const App = () => ( You can swich between different variations of your theme with the ["theme" prop](#switch-theme-variations). -### Custom CSS classes {#custom-css-classes} +### Custom CSS classes [#custom-css-classes] You can use custom CSS classes for the following elements: `"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`. @@ -224,7 +224,7 @@ const App = () => ( ) ``` -### Custom inline CSS {#custom-inline-styles} +### Custom inline CSS [#custom-inline-styles] You can use custom CSS inline styles for the following elements: `"button"`, `"container"`, `"anchor"`, `"divider"`, `"label"`, `"input"`, `"loader"`, `"message"`. @@ -249,7 +249,7 @@ const App = () => ( ) ``` -### Custom labels {#custom-labels} +### Custom labels [#custom-labels] You can use custom labels with `localization.variables`. See the [list of labels](https://github.com/supabase-community/auth-ui/blob/main/packages/react/common/lib/Localization/en.json) that can be overwritten. diff --git a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx index ed7fb35f055..62e6a0154e0 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/sveltekit.mdx @@ -371,9 +371,9 @@ export const handle: Handle = async ({ event, resolve }) => { } ``` -## Migrate from 0.7.x to 0.8 {#migration} +## Migrate from 0.7.x to 0.8 [#migration] -### Set up the Supabase client {#migration-set-up-supabase-client} +### Set up the Supabase client [#migration-set-up-supabase-client] -### Initialize the client {#migration-initialize-client} +### Initialize the client [#migration-initialize-client] -### Set up hooks {#migration-set-up-hooks} +### Set up hooks [#migration-set-up-hooks] -### Typings {#migration-typings} +### Typings [#migration-typings] -### withPageAuth {#migration-with-page-auth} +### withPageAuth [#migration-with-page-auth] { -### withApiAuth {#migration-with-api-auth} +### withApiAuth [#migration-with-api-auth] { -## Migrate from 0.6.11 and below to 0.7.0 {#migration-0-7} +## Migrate from 0.6.11 and below to 0.7.0 [#migration-0-7] There are numerous breaking changes in the latest 0.7.0 version of this library. @@ -720,7 +720,7 @@ There are numerous breaking changes in the latest 0.7.0 version of this library. The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`). -### Set up the Supabase client {#migration-set-up-supabase-client-0-7} +### Set up the Supabase client [#migration-set-up-supabase-client-0-7] -### Initialize the client {#migration-initialize-client-0-7} +### Initialize the client [#migration-initialize-client-0-7] -### Set up hooks {#migration-set-up-hooks-0-7} +### Set up hooks [#migration-set-up-hooks-0-7] -### Typings {#migration-typings-0-7} +### Typings [#migration-typings-0-7] -## `http_get` {#http_get} +## `http_get` [#http_get] Creates an HTTP GET request returning the request's ID. HTTP requests are not started until the transaction is committed. @@ -98,7 +98,7 @@ request_id After triggering `http_get`, use [`http_get_result`](#http_get_result) to get the result of the request. -## `http_post` {#http_post} +## `http_post` [#http_post] Creates an HTTP POST request with a JSON body, returning the request's ID. HTTP requests are not started until the transaction is committed. @@ -150,7 +150,7 @@ request_id After triggering `http_post`, use [`http_get_result`](#http_get_result) to get the result of the request. -## `http_collect_response` {#http_collect_response} +## `http_collect_response` [#http_collect_response] Given a `request_id` reference, retrieves the response. diff --git a/apps/docs/pages/guides/database/full-text-search.mdx b/apps/docs/pages/guides/database/full-text-search.mdx index bf25e78a453..51de6e5bdaa 100644 --- a/apps/docs/pages/guides/database/full-text-search.mdx +++ b/apps/docs/pages/guides/database/full-text-search.mdx @@ -55,7 +55,7 @@ values The functions we'll cover in this guide are: -### `to_tsvector()` {#to-tsvector} +### `to_tsvector()` [#to-tsvector] Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example: @@ -67,14 +67,14 @@ select to_tsvector('green eggs and ham') Collectively these tokens are called a "document" which Postgres can use for comparisons. -### `to_tsquery()` {#to-tsquery} +### `to_tsquery()` [#to-tsquery] Converts a query string into "tokens" to match. `to_tsquery()` stands for "to text search query". This conversion step is important because we will want to "fuzzy match" on keywords. For example if a user searches for "eggs", and a column has the value "egg", we probably still want to return a match. -### Match: `@@` {#match} +### Match: `@@` [#match] The `@@` symbol is the "match" symbol for Full Text Search. It returns any matches between a `to_tsvector` result and a `to_tsquery` result. @@ -449,7 +449,7 @@ final result = await client Visit [PostgreSQL: Text Search Functions and Operators](https://www.postgresql.org/docs/current/functions-textsearch.html) to learn about additional query operators you can use to do more advanced `full text queries`, such as: -### Proximity: `<->` {#proximity} +### Proximity: `<->` [#proximity] The proximity symbol is useful for searching for terms that are a certain "distance" apart. For example, to find the phrase `big dreams`, where the a match for "big" is followed immediately by a match for "dreams": @@ -538,7 +538,7 @@ final result = await client -### Negation: `!` {#negation} +### Negation: `!` [#negation] The negation symbol can be used to find phrases which _don't_ contain a search term. For example, to find records that have the word `big` but not `little`: diff --git a/apps/docs/pages/guides/migrations/firebase-auth.mdx b/apps/docs/pages/guides/migrations/firebase-auth.mdx index a054f68de51..6377ac83f13 100644 --- a/apps/docs/pages/guides/migrations/firebase-auth.mdx +++ b/apps/docs/pages/guides/migrations/firebase-auth.mdx @@ -12,7 +12,7 @@ Supabase provides several [tools](https://github.com/supabase-community/firebase - `firestoreusers2json` ([TypeScript](https://github.com/supabase-community/firebase-to-supabase/blob/main/auth/firestoreusers2json.ts), [JavaScript](https://github.com/supabase-community/firebase-to-supabase/blob/main/auth/firestoreusers2json.js)) exports users from an existing Firebase project to a `.json` file on your local system. - `import_users` ([TypeScript](https://github.com/supabase-community/firebase-to-supabase/blob/main/auth/import_users.ts), [JavaScript](https://github.com/supabase-community/firebase-to-supabase/blob/main/auth/import_users.js)) imports users from a saved `.json` file into your Supabase project (inserting those users into the `auth.users` table of your `PostgreSQL` database instance). -## Set up the migration tool {#set-up-migration-tool} +## Set up the migration tool [#set-up-migration-tool] 1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository: @@ -36,7 +36,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git 1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file. 1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file. -## Generate a Firebase private key {#generate-firebase-private-key} +## Generate a Firebase private key [#generate-firebase-private-key] 1. Log in to your [Firebase Console](https://console.firebase.google.com/project) and open your project. 1. Click the gear icon next to **Project Overview** in the sidebar and select **Project Settings**. @@ -44,7 +44,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git 1. Click **Generate new private key**. 1. Rename the downloaded file to `firebase-service.json`. -## Save your Firebase password hash parameters {#save-firebase-hash-parameters} +## Save your Firebase password hash parameters [#save-firebase-hash-parameters] 1. Log in to your [Firebase Console](https://console.firebase.google.com/project) and open your project. 1. Select **Authentication** (Build section) in the sidebar. @@ -64,14 +64,14 @@ hash_config { ## Command line options -### Dump Firestore users to a JSON file {#dump-firestore-users} +### Dump Firestore users to a JSON file [#dump-firestore-users] `node firestoreusers2json.js [] []` - `filename.json`: (optional) output filename (defaults to `./users.json`) - `batchSize`: (optional) number of users to fetch in each batch (defaults to 100) -### Import JSON users file to Supabase Auth (PostgreSQL: auth.users) {#import-json-users-file} +### Import JSON users file to Supabase Auth (PostgreSQL: auth.users) [#import-json-users-file] `node import_users.js []` diff --git a/apps/docs/pages/guides/migrations/firebase-storage.mdx b/apps/docs/pages/guides/migrations/firebase-storage.mdx index df644481f42..1516dd2537b 100644 --- a/apps/docs/pages/guides/migrations/firebase-storage.mdx +++ b/apps/docs/pages/guides/migrations/firebase-storage.mdx @@ -12,7 +12,7 @@ Supabase provides several [tools](https://github.com/supabase-community/firebase 1. Files are downloaded from a Firebase storage bucket to a local filesystem. 2. Files are uploaded from the local filesystem to a Supabase storage bucket. -## Set up the migration tool {#set-up-migration-tool} +## Set up the migration tool [#set-up-migration-tool] 1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository: @@ -25,7 +25,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git 1. Copy the **Project URL** and update the `SUPABASE_URL` value in `supabase-keys.js`. 1. Under **Project API keys**, copy the **service_role** key and update the `SUPABASE_KEY` value in `supabase-keys.js`. -## Generate a Firebase private key {#generate-firebase-private-key} +## Generate a Firebase private key [#generate-firebase-private-key] 1. Log in to your [Firebase Console](https://console.firebase.google.com/project) and open your project. 1. Click the gear icon next to **Project Overview** in the sidebar and select **Project Settings**. @@ -35,7 +35,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git ## Command line options -### Download Firestore Storage bucket to a local filesystem folder {#download-firestore-storage-bucket} +### Download Firestore Storage bucket to a local filesystem folder [#download-firestore-storage-bucket] `node download.js [] [] [] []` @@ -47,7 +47,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git To process in batches using multiple command-line executions, you must use the same parameters with a new `` on subsequent calls. Use the token displayed on the last call to continue the process at a given point. -### Upload files to Supabase Storage bucket {#upload-to-supabase-storage-bucket} +### Upload files to Supabase Storage bucket [#upload-to-supabase-storage-bucket] `node upload.js ` diff --git a/apps/docs/pages/guides/migrations/firestore-data.mdx b/apps/docs/pages/guides/migrations/firestore-data.mdx index 936c8753758..5e99d591e95 100644 --- a/apps/docs/pages/guides/migrations/firestore-data.mdx +++ b/apps/docs/pages/guides/migrations/firestore-data.mdx @@ -11,7 +11,7 @@ Supabase provides several [tools](https://github.com/supabase-community/firebase The Firestore `collection` is "flattened" and converted to a table with basic columns of one of the following types: `text`, `numeric`, `boolean`, or `jsonb`. If your structure is more complex, you can write a program to split the newly-created `json` file into multiple, related tables before you import your `json` file(s) to Supabase. -## Set up the migration tool {#set-up-migration-tool} +## Set up the migration tool [#set-up-migration-tool] 1. Clone the [firebase-to-supabase](https://github.com/supabase-community/firebase-to-supabase) repository: @@ -35,7 +35,7 @@ git clone https://github.com/supabase-community/firebase-to-supabase.git 1. Under **Connection Info**, copy the Host string and replace the entry in your `supabase-service.json` file. 1. Enter the password you used when you created your Supabase project in the `password` entry in the `supabase-service.json` file. -## Generate a Firebase private key {#generate-firebase-private-key} +## Generate a Firebase private key [#generate-firebase-private-key] 1. Log in to your [Firebase Console](https://console.firebase.google.com/project) and open your project. 1. Click the gear icon next to **Project Overview** in the sidebar and select **Project Settings**. @@ -79,7 +79,7 @@ And split it into two files (one table for users and one table for items): ] ``` -### Import JSON file to Supabase (PostgreSQL) {#import-to-supabase} +### Import JSON file to Supabase (PostgreSQL) [#import-to-supabase] `node json2supabase.js [] []` diff --git a/apps/docs/pages/guides/migrations/heroku.mdx b/apps/docs/pages/guides/migrations/heroku.mdx index 9ab54a1ce4a..51ab83dfb49 100644 --- a/apps/docs/pages/guides/migrations/heroku.mdx +++ b/apps/docs/pages/guides/migrations/heroku.mdx @@ -22,7 +22,7 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab > -## Retrieve your Heroku database credentials {#retrieve-heroku-credentials} +## Retrieve your Heroku database credentials [#retrieve-heroku-credentials] 1. Log in to your [Heroku account](https://heroku.com) and select the project you want to migrate. 1. Click **Resources** in the menu and select your **Heroku Postgres** database. @@ -33,13 +33,13 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab - User (`$HEROKU_USER`) - Password (`$HEROKU_PASSWORD`) -## Retrieve your Supabase Host {#retrieve-supabase-host} +## Retrieve your Supabase Host [#retrieve-supabase-host] 1. If you're new to Supabase, [create a project](https://app.supabase.com). 1. Go to the [Database settings](https://app.supabase.com/project/_/settings/database) for your project in the Supabase Dashboard. 1. Under **Connection Info**, note your Host (`$SUPABASE_HOST`). -## Export your Heroku database to a file {#export-heroku-database} +## Export your Heroku database to a file [#export-heroku-database] Use `pg_dump` with your Heroku credentials to export your Heroku database to a file (e.g., `heroku_dump.sql`). @@ -49,7 +49,7 @@ pg_dump --clean --if-exists --quote-all-identifiers \ --no-owner --no-privileges > heroku_dump.sql ``` -## Import the database to your Supabase project {#import-database-to-supabase} +## Import the database to your Supabase project [#import-database-to-supabase] Use `psql` to import the Heroku database file to your Supabase project. diff --git a/apps/docs/pages/guides/platform/compute-add-ons.mdx b/apps/docs/pages/guides/platform/compute-add-ons.mdx index d175d4457c3..1aa251dd08b 100644 --- a/apps/docs/pages/guides/platform/compute-add-ons.mdx +++ b/apps/docs/pages/guides/platform/compute-add-ons.mdx @@ -3,23 +3,23 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'compute-add-ons', title: 'Compute Add-ons', - description: 'Learn about your project\'s instance and additional add-ons.', + description: "Learn about your project's instance and additional add-ons.", } Every project on the Supabase Platform comes with its own dedicated Postgres instance running inside a virtual machine (VM). The following table describes the base instance with additional compute add-ons available if you need extra performance when scaling up Supabase. -Plan | Pricing | CPU | Memory | Disk IO Bandwidth | Connections: Direct | Connections: Pooler ------|---------|-----|--------|-------------------|---------------------|-------------------- -Free (Included) | $0 | 2-core ARM (shared) | 1 GB | Up to 2,085 Mbps | 10 (recommended) | 50 (recommended) -Small | $5 | 2-core ARM (shared) | 2 GB | Up to 2,085 Mbps | 30 (recommended) | 75 (recommended) -Medium | $50 | 2-core ARM (shared) | 4 GB | Up to 2,085 Mbps | 50 (recommended) | 150 (recommended) -Large | $100 | 2-core ARM (dedicated) | 8 GB | Up to 4,750 Mbps | 100 (recommended) | 300 (recommended) -XL | $200 | 4-core ARM (dedicated) | 16 GB | Up to 4,750 Mbps | 200 (recommended) | 600 (recommended) -2XL | $400 | 8-core ARM (dedicated) | 32 GB | Up to 4,750 Mbps | 350 (recommended) | 1200 (recommended) -4XL | $950 | 16-core ARM (dedicated) | 64 GB | Up to 4,750 Mbps | 420 (recommended) | 2800 (recommended) -8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | Up to 9,000 Mbps | 450 (recommended) | 5600 (recommended) -12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | Up to 13,500 Mbps | 480 (recommended) | 8600 (recommended) -16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | Up to 19,000 Mbps | 500 (recommended) | 11,600 (recommended) +| Plan | Pricing | CPU | Memory | Disk IO Bandwidth | Connections: Direct | Connections: Pooler | +| --------------- | ------- | ----------------------- | ------ | ----------------- | ------------------- | -------------------- | +| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | Up to 2,085 Mbps | 10 (recommended) | 50 (recommended) | +| Small | $5 | 2-core ARM (shared) | 2 GB | Up to 2,085 Mbps | 30 (recommended) | 75 (recommended) | +| Medium | $50 | 2-core ARM (shared) | 4 GB | Up to 2,085 Mbps | 50 (recommended) | 150 (recommended) | +| Large | $100 | 2-core ARM (dedicated) | 8 GB | Up to 4,750 Mbps | 100 (recommended) | 300 (recommended) | +| XL | $200 | 4-core ARM (dedicated) | 16 GB | Up to 4,750 Mbps | 200 (recommended) | 600 (recommended) | +| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | Up to 4,750 Mbps | 350 (recommended) | 1200 (recommended) | +| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | Up to 4,750 Mbps | 420 (recommended) | 2800 (recommended) | +| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | Up to 9,000 Mbps | 450 (recommended) | 5600 (recommended) | +| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | Up to 13,500 Mbps | 480 (recommended) | 8600 (recommended) | +| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | Up to 19,000 Mbps | 500 (recommended) | 11,600 (recommended) | [Contact us](https://supabase.com/contact/enterprise) if you require a custom plan. @@ -27,7 +27,7 @@ XL | $200 | 4-core ARM (dedicated) | 16 GB | Up to 4,750 Mbps | 200 (recommended All Postgres instances on Supabase are dedicated applications running inside dedicated virtual machines. However, the underlying hardware resources, for example the physical CPU, may be shared between multiple VMs, but appear to the OS as if it is a dedicated hardware CPU. This is commonly referred to as a vCPU (virtual CPU). Cloud providers use these shared hardware resources to save cost—you can upgrade to a larger compute add-on to guarantee a dedicated physical CPU for your instance. -## Compute upgrades {#upgrades} +## Compute upgrades [#upgrades] When considering compute upgrades, assess whether your bottlenecks are hardware-constrained or software-constrained. For example, you may want to look into [optimizing the number of connections](/docs/guides/platform/performance#optimizing-the-number-of-connections) or [examining query performance](/docs/guides/platform/performance#examining-query-performance). When you're happy with your Postgres instance's performance, then you can focus on additional compute resources. For example, you can load test your application in staging to understand your compute requirements. You can also start out on a smaller tier, [create a report](https://app.supabase.com/project/_/reports) in the Dashboard to monitor your CPU utilization, and upgrade later as needed diff --git a/apps/docs/pages/guides/with-flutter.mdx b/apps/docs/pages/guides/with-flutter.mdx index dab40d95197..db3e028ad30 100644 --- a/apps/docs/pages/guides/with-flutter.mdx +++ b/apps/docs/pages/guides/with-flutter.mdx @@ -794,6 +794,8 @@ Congratulations, that is it! You have now built a fully functional user manageme ## See also - [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app) +- [Flutter Tutorial - Part 2: Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls) + export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/with-nextjs.mdx b/apps/docs/pages/guides/with-nextjs.mdx index 35883c7ffcf..df4d8171a4d 100644 --- a/apps/docs/pages/guides/with-nextjs.mdx +++ b/apps/docs/pages/guides/with-nextjs.mdx @@ -111,10 +111,7 @@ function MyApp({ Component, pageProps }) { const [supabase] = useState(() => createBrowserSupabaseClient()) return ( - + ) @@ -140,10 +137,7 @@ function MyApp({ const [supabase] = useState(() => createBrowserSupabaseClient()) return ( - + ) diff --git a/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx b/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx index a6349c9cec5..c459d5edfa2 100644 --- a/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx +++ b/apps/www/_blog/2021-07-27-spot-flutter-with-postgres.mdx @@ -86,7 +86,7 @@ With Supabase, Tyler can use Flutter to build a performant app quickly at a low experience and has even joined the SupaSquad. > Update: After being part of the SupaSquad, Tyler joined Supabase full time as part of the DevRel squad, focusing on Flutter. This is some of his latest work: [Flutter Tutorial: building a Flutter chat app -> ](https://supabase.com/blog/flutter-tutorial-building-a-chat-app). +> ](https://supabase.com/blog/flutter-tutorial-building-a-chat-app) and [Flutter Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls). You can download and install spot for [iOS](https://apps.apple.com/us/app/spot-videos/id1564675926?utm_source=supabase&utm_campaign=static) and [Android](https://play.google.com/store/apps/details?id=app.spotvideo&utm_source=supabase&utm_campaign=static) diff --git a/apps/www/_blog/2022-06-30-flutter-tutorial-building-a-chat-app.mdx b/apps/www/_blog/2022-06-30-flutter-tutorial-building-a-chat-app.mdx index e777171608f..8285a717828 100644 --- a/apps/www/_blog/2022-06-30-flutter-tutorial-building-a-chat-app.mdx +++ b/apps/www/_blog/2022-06-30-flutter-tutorial-building-a-chat-app.mdx @@ -16,6 +16,8 @@ This is the first of a series of Flutter Tutorials. During the series, you will In the first part, I will show you how to build a chat application, where users can sign up and talk to other users in a single chat room. The exchange of messages will happen in real time, meaning that you do not have to refresh the page to load new messages. +In the second part, I will show you how to implement [Authentication and Authorization (with RLS)](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls) to the chat app. + In order to build this chat app, we will need a database to store all of the chat information. We will be using [Supabase](https://supabase.com/) to store the chat information. Supabase is a back end as a service that provides auth, database, storage and functions to easily create a scalable application. Supabase is a suitable backend for this app, because it provides a nice set of APIs on Postgres database that we can easily call by using the SDK. It is also perfect for chat apps like the one we are about to create, because we can subscribe to real time changes on the database. This article also assumes that you have installed Flutter already on your machine. If not, you can go to the [official Flutter page](https://docs.flutter.dev/get-started/install) to start the installation. @@ -885,6 +887,7 @@ If you have any questions please reach out via [Twitter](https://twitter.com/dsh ## More Flutter Resources +- [Part 2: Flutter Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls) - [Flutter Supabase Quick Starter Guide](https://supabase.com/docs/guides/with-flutter) - [supabase-flutter v1.0 released](https://supabase.com/blog/supabase-flutter-sdk-v1-released) - [Open source map based Flutter app with CI/CD and scalable statemanagement example](https://github.com/dshukertjr/spot) diff --git a/apps/www/_blog/2022-08-02-supabase-flutter-sdk-1-developer-preview.mdx b/apps/www/_blog/2022-08-02-supabase-flutter-sdk-1-developer-preview.mdx index 078d2e0134a..39f8595377c 100644 --- a/apps/www/_blog/2022-08-02-supabase-flutter-sdk-1-developer-preview.mdx +++ b/apps/www/_blog/2022-08-02-supabase-flutter-sdk-1-developer-preview.mdx @@ -159,4 +159,5 @@ If you have any feedbacks, please let us know in the issues of the [supabase-flu - [supabase-flutter 1.0 developer preview](https://pub.dev/packages/supabase_flutter) - [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app) +- [Flutter Tutorial - Part 2: Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls) - [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase) diff --git a/apps/www/_blog/2022-10-21-supabase-flutter-sdk-v1-released.mdx b/apps/www/_blog/2022-10-21-supabase-flutter-sdk-v1-released.mdx index 723c84f3e7a..e7fdabdc869 100644 --- a/apps/www/_blog/2022-10-21-supabase-flutter-sdk-v1-released.mdx +++ b/apps/www/_blog/2022-10-21-supabase-flutter-sdk-v1-released.mdx @@ -99,4 +99,5 @@ It required massive support from the community to bring the supabase-flutter to - [supabase-flutter documentation](https://supabase.com/docs/reference/dart/) - [v0 to v1 migration guide](https://supabase.com/docs/reference/dart/upgrade-guide) - [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app) +- [Flutter Tutorial - Part 2: Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls) - [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase) diff --git a/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx new file mode 100644 index 00000000000..9faac610045 --- /dev/null +++ b/apps/www/_blog/2022-11-22-flutter-authentication-and-authorization-with-rls.mdx @@ -0,0 +1,1362 @@ +--- +title: 'Flutter Authentication and Authorization with RLS' +description: Learn how you can secure your Flutter app using Supabase Row Level Security. +author: tyler_shukert +image: flutter-chat-auth/supabase-flutter-auth.jpeg +thumb: flutter-chat-auth/supabase-flutter-auth.jpeg +tags: + - flutter + - auth + - mobile +date: '2022-11-22' +toc_depth: 3 +--- + +This article is the second part of the Flutter tutorial series. During the series, you will learn how to build cross-platform apps without worrying about the backend. + +In this article, I will show you how you can make a secure chat application by introducing authentication and authorization to the basic chat app that we created [previously](https://supabase.com/blog/flutter-tutorial-building-a-chat-app). + +We will use [Supabase](https://supabase.com/) to store the chat data and will use its [auth feature](https://supabase.com/auth) to provide a secure chat room feature. +Supabase lets us build a secure database where we can control who can access what part of the database, so your users’ data is secured without worrying about the backend. + +## What we created in the previous article + +Before we jump in, let’s go over what we built in the [previous article](blog/flutter-tutorial-building-a-chat-app), because we will be building on top of it. If you have not gone through it, I recommend you to go check it out. + +In the previous article, we created a basic real-time chat application. Users will register or sign in using an email address and password. Once they are signed in, they are taken to a chat page, where they can view and send messages to everyone in the app. There are no Chat rooms, and everyone’s messages were sent to the same chat room. + +You can also find a complete code example [here](https://github.com/supabase-community/flutter-chat/tree/with_auth) to follow along. + +![Chat app without authorization](/images/blog/flutter-chat-auth/chat-app-screenshot.png) + +## Overview of the final app + +The app will allow us to have 1 on 1 chat with other users in the app. To enable this, we will introduce a new rooms page. The rooms page serves two purposes here, one is to initiate a conversation with other users, and the other is to display existing chat rooms. At the top of the app, we see a list of other users’ icons. A user can tap the icon to start a 1 on 1 conversation. Below the icons, there is a list of rooms that the user is a part of. + +![Chat app without authorization](/images/blog/flutter-chat-auth/rooms-and-chat-page.png) + +## Sessing up the scene + +### Install additional dependencies + +We will install flutter_bloc for state management. +Introducing a state management solution will allow us to handle the shared message and profile data efficiently between the rooms page and the chats page. +We can use any state management solution for this, but we are going with bloc in this example. +Add the following in your pubspec.yaml file to install flutter_bloc in your app. + +```dart +flutter_bloc: ^8.0.0 +``` + +### Modifying the table schema + +Since the app has evolved, we also need to update our table schema. In order to store rooms data, we will add a rooms table. We will also modify the messages table to add a foreign key constraint to the rooms table so that we can tell which message belongs to which room. + +We will also intorudce a `create_new_room` function, which is a [database function](https://supabase.com/docs/guides/database/functions) that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists. + +```sql +-- *** Table definitions *** + +create table if not exists public.rooms ( + id uuid not null primary key default uuid_generate_v4(), + created_at timestamp with time zone default timezone('utc' :: text, now()) not null +); +comment on table public.rooms is 'Holds chat rooms'; + +create table if not exists public.room_participants ( + profile_id uuid references public.profiles(id) on delete cascade not null, + room_id uuid references public.rooms(id) on delete cascade not null, + created_at timestamp with time zone default timezone('utc' :: text, now()) not null, + primary key (profile_id, room_id) +); +comment on table public.room_participants is 'Relational table of users and rooms.'; + +alter table public.messages +add column room_id uuid references public.rooms(id) on delete cascade not null; + +-- *** Add tables to the publication to enable realtime *** + +alter publication supabase_realtime add table public.room_participants; + +-- Creates a new room with the user and another user in it. +-- Will return the room_id of the created room +-- Will return a room_id if there were already a room with those participants +create or replace function create_new_room(other_user_id uuid) returns uuid as $$ + declare + new_room_id uuid; + begin + -- Check if room with both participants already exist + with rooms_with_profiles as ( + select room_id, array_agg(profile_id) as participants + from room_participants + group by room_id + ) + select room_id + into new_room_id + from rooms_with_profiles + where create_new_room.other_user_id=any(participants) + and auth.uid()=any(participants); + + + if not found then + -- Create a new room + insert into public.rooms default values + returning id into new_room_id; + + -- Insert the caller user into the new room + insert into public.room_participants (profile_id, room_id) + values (auth.uid(), new_room_id); + + -- Insert the other_user user into the new room + insert into public.room_participants (profile_id, room_id) + values (other_user_id, new_room_id); + end if; + + return new_room_id; + end +$$ language plpgsql security definer; +``` + +### Setup deep links + +Something we skipped in the previous article was sending confirmation emails to users when they signup. Since today is about security, let's properly send confirmation emails to people who signup. + +When we send confirmation emails, the users need to be brought back to the app somehow. +Since supabase_flutter has a mechanism to detect and handle deep links, we will register a `io.supabase.chat://login` as our deep link for the app and bring the users back after confirming their email address. + +For iOS we edit the info.plist file to register the deep link. + +```xml title=ios/Runner/Info.plist" + + + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + io.supabase.chat + + + + + + +``` + +For Android we edit the AndroidManifest.xml to register the deep link. + +```xml title=android/app/src/main/AndroidManifest.xml + + + + + + + + + + + + + + + + + + +``` + +We also need to set the deep link in our Supabase dashboard. Go to Authentication > URL Configuration in your dashboard and add `io.supabase.chat://login` as one of the redirect URLs. + +![Deep link URL Configuration](/images/blog/flutter-chat-auth/url-configuration.png) + +And that is it for deep link configuration. + +## Building out the main application + +### Step1: Create rooms page + +The rooms page will load two types of data, recently added users and a list of rooms that the user belongs to. We will be using bloc to load these two types of data and display them on the rooms page. + +Let’s start out by creating states for the rooms page. +The rooms page would have four different states, loading, loaded, empty, and error. We will display different UI on the rooms page depending on what state it is. +Create a `lib/cubit/rooms/rooms_state.dart` file and paste the following code. +You may see some errors, but we will take care of them in the next step. + +```dart title=lib/cubit/rooms/rooms_state.dart +part of 'rooms_cubit.dart'; + +@immutable +abstract class RoomState {} + +class RoomsLoading extends RoomState {} + +class RoomsLoaded extends RoomState { + final List newUsers; + final List rooms; + + RoomsLoaded({ + required this.rooms, + required this.newUsers, + }); +} + +class RoomsEmpty extends RoomState { + final List newUsers; + + RoomsEmpty({required this.newUsers}); +} + +class RoomsError extends RoomState { + final String message; + + RoomsError(this.message); +} +``` + +Now that we have the states defined, we will create rooms_cubit. +A [cubit](https://bloclibrary.dev/#/coreconcepts?id=cubit) is a class within the flutter_bloc library where we will make requests to Supabase to get the data and transform them into states and emit them to the UI widgets. +Let's create a `lib/cubit/rooms/rooms_cubit.dart` file and complete the cubit. + +```dart title=lib/cubit/rooms/rooms_cubit.dart +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart'; +import 'package:my_chat_app/models/profile.dart'; +import 'package:my_chat_app/models/message.dart'; +import 'package:my_chat_app/models/room.dart'; +import 'package:my_chat_app/utils/constants.dart'; + +part 'rooms_state.dart'; + +class RoomCubit extends Cubit { + RoomCubit() : super(RoomsLoading()); + + final Map> + _messageSubscriptions = {}; + + late final String _myUserId; + + /// List of new users of the app for the user to start talking to + late final List _newUsers; + + /// List of rooms + List _rooms = []; + StreamSubscription>>? + _rawRoomsSubscription; + bool _haveCalledGetRooms = false; + + Future initializeRooms(BuildContext context) async { + if (_haveCalledGetRooms) { + return; + } + _haveCalledGetRooms = true; + + _myUserId = supabase.auth.currentUser!.id; + + late final List data; + + try { + data = await supabase + .from('profiles') + .select() + .not('id', 'eq', _myUserId) + .order('created_at') + .limit(12); + } catch (_) { + emit(RoomsError('Error loading new users')); + } + + final rows = List>.from(data); + _newUsers = rows.map(Profile.fromMap).toList(); + + /// Get realtime updates on rooms that the user is in + _rawRoomsSubscription = + supabase.from('room_participants').stream( + primaryKey: ['room_id', 'profile_id'], + ).listen((participantMaps) async { + if (participantMaps.isEmpty) { + emit(RoomsEmpty(newUsers: _newUsers)); + return; + } + + _rooms = participantMaps + .map(Room.fromRoomParticipants) + .where((room) => room.otherUserId != _myUserId) + .toList(); + for (final room in _rooms) { + _getNewestMessage( + context: context, roomId: room.id); + BlocProvider.of(context) + .getProfile(room.otherUserId); + } + emit(RoomsLoaded( + newUsers: _newUsers, + rooms: _rooms, + )); + }, onError: (error) { + emit(RoomsError('Error loading rooms')); + }); + } + + // Setup listeners to listen to the most recent message in each room + void _getNewestMessage({ + required BuildContext context, + required String roomId, + }) { + _messageSubscriptions[roomId] = supabase + .from('messages') + .stream(primaryKey: ['id']) + .eq('room_id', roomId) + .order('created_at') + .limit(1) + .map( + (data) => data.isEmpty + ? null + : Message.fromMap( + map: data.first, + myUserId: _myUserId, + ), + ) + .listen((message) { + final index = _rooms + .indexWhere((room) => room.id == roomId); + _rooms[index] = + _rooms[index].copyWith(lastMessage: message); + _rooms.sort((a, b) { + /// Sort according to the last message + /// Use the room createdAt when last message is not available + final aTimeStamp = a.lastMessage != null + ? a.lastMessage!.createdAt + : a.createdAt; + final bTimeStamp = b.lastMessage != null + ? b.lastMessage!.createdAt + : b.createdAt; + return bTimeStamp.compareTo(aTimeStamp); + }); + if (!isClosed) { + emit(RoomsLoaded( + newUsers: _newUsers, + rooms: _rooms, + )); + } + }); + } + + /// Creates or returns an existing roomID of both participants + Future createRoom(String otherUserId) async { + final data = await supabase.rpc('create_new_room', + params: {'other_user_id': otherUserId}); + emit(RoomsLoaded(rooms: _rooms, newUsers: _newUsers)); + return data as String; + } + + @override + Future close() { + _rawRoomsSubscription?.cancel(); + return super.close(); + } +} +``` + +Now that we have the states and cubit to power our rooms page, it’s time to create the `RoomsPage`. + +We have two list views, one horizontal list view to display other users, and one vertical list views with list tiles representing each room that the user is a part of. +We will create a `lib/pages/rooms_page.dart` file with the following content. + +```dart title=lib/pages/rooms_page.dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart'; + +import 'package:my_chat_app/cubits/rooms/rooms_cubit.dart'; +import 'package:my_chat_app/models/profile.dart'; +import 'package:my_chat_app/pages/chat_page.dart'; +import 'package:my_chat_app/pages/register_page.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:timeago/timeago.dart'; + +/// Displays the list of chat threads +class RoomsPage extends StatelessWidget { + const RoomsPage({Key? key}) : super(key: key); + + static Route route() { + return MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => + RoomCubit()..initializeRooms(context), + child: const RoomsPage(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Rooms'), + actions: [ + TextButton( + onPressed: () async { + await supabase.auth.signOut(); + Navigator.of(context).pushAndRemoveUntil( + RegisterPage.route(), + (route) => false, + ); + }, + child: const Text('Logout'), + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + if (state is RoomsLoading) { + return preloader; + } else if (state is RoomsLoaded) { + final newUsers = state.newUsers; + final rooms = state.rooms; + return BlocBuilder( + builder: (context, state) { + if (state is ProfilesLoaded) { + final profiles = state.profiles; + return Column( + children: [ + _NewUsers(newUsers: newUsers), + Expanded( + child: ListView.builder( + itemCount: rooms.length, + itemBuilder: (context, index) { + final room = rooms[index]; + final otherUser = + profiles[room.otherUserId]; + + return ListTile( + onTap: () => + Navigator.of(context) + .push(ChatPage.route( + room.id)), + leading: CircleAvatar( + child: otherUser == null + ? preloader + : Text(otherUser + .username + .substring(0, 2)), + ), + title: Text(otherUser == null + ? 'Loading...' + : otherUser.username), + subtitle: room.lastMessage != + null + ? Text( + room.lastMessage! + .content, + maxLines: 1, + overflow: TextOverflow + .ellipsis, + ) + : const Text( + 'Room created'), + trailing: Text(format( + room.lastMessage + ?.createdAt ?? + room.createdAt, + locale: 'en_short')), + ); + }, + ), + ), + ], + ); + } else { + return preloader; + } + }, + ); + } else if (state is RoomsEmpty) { + final newUsers = state.newUsers; + return Column( + children: [ + _NewUsers(newUsers: newUsers), + const Expanded( + child: Center( + child: Text( + 'Start a chat by tapping on available users'), + ), + ), + ], + ); + } else if (state is RoomsError) { + return Center(child: Text(state.message)); + } + throw UnimplementedError(); + }, + ), + ); + } +} + +class _NewUsers extends StatelessWidget { + const _NewUsers({ + Key? key, + required this.newUsers, + }) : super(key: key); + + final List newUsers; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 8), + scrollDirection: Axis.horizontal, + child: Row( + children: newUsers + .map((user) => InkWell( + onTap: () async { + try { + final roomId = + await BlocProvider.of( + context) + .createRoom(user.id); + Navigator.of(context) + .push(ChatPage.route(roomId)); + } catch (_) { + context.showErrorSnackBar( + message: + 'Failed creating a new room'); + } + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 60, + child: Column( + children: [ + CircleAvatar( + child: Text(user.username + .substring(0, 2)), + ), + const SizedBox(height: 8), + Text( + user.username, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + )) + .toList(), + ), + ); + } +} +``` + +You may see some errors, but they will go away once we edit the chat page! + +### Step 2: Modify the chat page to load messages in the room + +Our `ChatPage` will have a similar layout as the previous one, but will only display messages sent to a single room. We will start by creating `MessagesState`. The messages page will also have four different states, loading, loaded, empty, and error. +Create a `lib/cubits/chat/chat_state.dart` file with the following code. + +```dart title=lib/cubits/chat/chat_state.dart +part of 'chat_cubit.dart'; + +@immutable +abstract class ChatState {} + +class ChatInitial extends ChatState {} + +class ChatLoaded extends ChatState { + ChatLoaded(this.messages); + final List messages; +} + +class ChatEmpty extends ChatState {} + +class ChatError extends ChatState { + ChatError(this.message); + final String message; +} +``` + +Now let's create chat cubit to retrieve the data from our database and emit it as states. +Create a `lib/cubits/chat/chat_cubit.dart` file and paste the following. + +```dart title=lib/cubits/chat/chat_cubit.dart +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:my_chat_app/models/message.dart'; +import 'package:my_chat_app/utils/constants.dart'; + +part 'chat_state.dart'; + +class ChatCubit extends Cubit { + ChatCubit() : super(ChatInitial()); + + StreamSubscription>? _messagesSubscription; + List _messages = []; + + late final String _roomId; + late final String _myUserId; + + void setMessagesListener(String roomId) { + _roomId = roomId; + + _myUserId = supabase.auth.currentUser!.id; + + _messagesSubscription = supabase + .from('messages') + .stream(primaryKey: ['id']) + .eq('room_id', roomId) + .order('created_at') + .map>( + (data) => data + .map( + (row) => Message.fromMap(map: row, myUserId: _myUserId)) + .toList(), + ) + .listen((messages) { + _messages = messages; + if (_messages.isEmpty) { + emit(ChatEmpty()); + } else { + emit(ChatLoaded(_messages)); + } + }); + } + + Future sendMessage(String text) async { + /// Add message to present to the user right away + final message = Message( + id: 'new', + roomId: _roomId, + profileId: _myUserId, + content: text, + createdAt: DateTime.now(), + isMine: true, + ); + _messages.insert(0, message); + emit(ChatLoaded(_messages)); + + try { + await supabase.from('messages').insert(message.toMap()); + } catch (_) { + emit(ChatError('Error submitting message.')); + _messages.removeWhere((message) => message.id == 'new'); + emit(ChatLoaded(_messages)); + } + } + + @override + Future close() { + _messagesSubscription?.cancel(); + return super.close(); + } +} +``` + +Chat cubit is pretty simple. It sets a real-time listener to the database using the stream method and emits an empty state if there are no messages in the room, or emits a loaded state if there are messages. + +Because we are using cubit, we need to modify the MessagesPage widget as well. +Open `lib/pages/chat_page.dart` and let's update it. + +```dart title=lib/pages/chat_page.dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_chat_app/components/user_avatar.dart'; +import 'package:my_chat_app/cubits/chat/chat_cubit.dart'; + +import 'package:my_chat_app/models/message.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:timeago/timeago.dart'; + +/// Page to chat with someone. +/// +/// Displays chat bubbles as a ListView and TextField to enter new chat. +class ChatPage extends StatelessWidget { + const ChatPage({Key? key}) : super(key: key); + + static Route route(String roomId) { + return MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => ChatCubit()..setMessagesListener(roomId), + child: const ChatPage(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Chat')), + body: BlocConsumer( + listener: (context, state) { + if (state is ChatError) { + context.showErrorSnackBar(message: state.message); + } + }, + builder: (context, state) { + if (state is ChatInitial) { + return preloader; + } else if (state is ChatLoaded) { + final messages = state.messages; + return Column( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + reverse: true, + itemCount: messages.length, + itemBuilder: (context, index) { + final message = messages[index]; + return _ChatBubble(message: message); + }, + ), + ), + const _MessageBar(), + ], + ); + } else if (state is ChatEmpty) { + return Column( + children: const [ + Expanded( + child: Center( + child: Text('Start your conversation now :)'), + ), + ), + _MessageBar(), + ], + ); + } else if (state is ChatError) { + return Center(child: Text(state.message)); + } + throw UnimplementedError(); + }, + ), + ); + } +} + +/// Set of widget that contains TextField and Button to submit message +class _MessageBar extends StatefulWidget { + const _MessageBar({ + Key? key, + }) : super(key: key); + + @override + State<_MessageBar> createState() => _MessageBarState(); +} + +class _MessageBarState extends State<_MessageBar> { + late final TextEditingController _textController; + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).cardColor, + child: Padding( + padding: EdgeInsets.only( + top: 8, + left: 8, + right: 8, + bottom: MediaQuery.of(context).padding.bottom, + ), + child: Row( + children: [ + Expanded( + child: TextFormField( + keyboardType: TextInputType.text, + maxLines: null, + autofocus: true, + controller: _textController, + decoration: const InputDecoration( + hintText: 'Type a message', + border: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.all(8), + ), + ), + ), + TextButton( + onPressed: () => _submitMessage(), + child: const Text('Send'), + ), + ], + ), + ), + ); + } + + @override + void initState() { + _textController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + void _submitMessage() async { + final text = _textController.text; + if (text.isEmpty) { + return; + } + BlocProvider.of(context).sendMessage(text); + _textController.clear(); + } +} + +class _ChatBubble extends StatelessWidget { + const _ChatBubble({ + Key? key, + required this.message, + }) : super(key: key); + + final Message message; + + @override + Widget build(BuildContext context) { + List chatContents = [ + if (!message.isMine) UserAvatar(userId: message.profileId), + const SizedBox(width: 12), + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + color: message.isMine + ? Colors.grey[300] + : Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text(message.content), + ), + ), + const SizedBox(width: 12), + Text(format(message.createdAt, locale: 'en_short')), + const SizedBox(width: 60), + ]; + if (message.isMine) { + chatContents = chatContents.reversed.toList(); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 18), + child: Row( + mainAxisAlignment: + message.isMine ? MainAxisAlignment.end : MainAxisAlignment.start, + children: chatContents, + ), + ); + } +} +``` + +### Step 3: Implementing Authentication in Flutter + +Because we have modified the setting of our Supabase to send a confirmation email, we need to make some modifications to the register page and login page as well. + +The main change is how we handle navigation. +Previously, we were able to navigate the user to `ChatPage` right after sign-in was complete. +This would no longer work, as we now have to wait for the user to confirm their email address. +In this case, we would want to listen to auth state of the user and navigate when the user is signed in with a session. +This allows us to react when the user confirmed their email addresses. + +```dart title=lib/pages/register_page.dart +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:my_chat_app/pages/login_page.dart'; +import 'package:my_chat_app/pages/rooms_page.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage( + {Key? key, required this.isRegistering}) + : super(key: key); + + static Route route({bool isRegistering = false}) { + return MaterialPageRoute( + builder: (context) => + RegisterPage(isRegistering: isRegistering), + ); + } + + final bool isRegistering; + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final bool _isLoading = false; + + final _formKey = GlobalKey(); + + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _usernameController = TextEditingController(); + + late final StreamSubscription + _authSubscription; + + @override + void initState() { + super.initState(); + + bool haveNavigated = false; + // Listen to auth state to redirect user when the user clicks on confirmation link + _authSubscription = + supabase.auth.onAuthStateChange.listen((data) { + final session = data.session; + if (session != null && !haveNavigated) { + haveNavigated = true; + Navigator.of(context) + .pushReplacement(RoomsPage.route()); + } + }); + } + + @override + void dispose() { + super.dispose(); + + // Dispose subscription when no longer needed + _authSubscription.cancel(); + } + + Future _signUp() async { + final isValid = _formKey.currentState!.validate(); + if (!isValid) { + return; + } + final email = _emailController.text; + final password = _passwordController.text; + final username = _usernameController.text; + try { + await supabase.auth.signUp( + email: email, + password: password, + data: {'username': username}, + emailRedirectTo: 'io.supabase.chat://login', + ); + context.showSnackBar( + message: + 'Please check your inbox for confirmation email.'); + } on AuthException catch (error) { + context.showErrorSnackBar(message: error.message); + } catch (error) { + debugPrint(error.toString()); + context.showErrorSnackBar( + message: unexpectedErrorMessage); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Register'), + ), + body: Form( + key: _formKey, + child: ListView( + padding: formPadding, + children: [ + TextFormField( + controller: _emailController, + decoration: const InputDecoration( + label: Text('Email'), + ), + validator: (val) { + if (val == null || val.isEmpty) { + return 'Required'; + } + return null; + }, + keyboardType: TextInputType.emailAddress, + ), + spacer, + TextFormField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + label: Text('Password'), + ), + validator: (val) { + if (val == null || val.isEmpty) { + return 'Required'; + } + if (val.length < 6) { + return '6 characters minimum'; + } + return null; + }, + ), + spacer, + TextFormField( + controller: _usernameController, + decoration: const InputDecoration( + label: Text('Username'), + ), + validator: (val) { + if (val == null || val.isEmpty) { + return 'Required'; + } + final isValid = + RegExp(r'^[A-Za-z0-9_]{3,24}$') + .hasMatch(val); + if (!isValid) { + return '3-24 long with alphanumeric or underscore'; + } + return null; + }, + ), + spacer, + ElevatedButton( + onPressed: _isLoading ? null : _signUp, + child: const Text('Register'), + ), + spacer, + TextButton( + onPressed: () { + Navigator.of(context) + .push(LoginPage.route()); + }, + child: + const Text('I already have an account')) + ], + ), + ), + ); + } +} +``` + +Login page becomes more simple. +All it is doing is taking a user’s email and password and logging them in. +It is not doing any navigation whatsoever. +This is because `LoginPage` is navigated on top of `RegisterPage`, the auth state listener on `RegisterPage` is still active, and therefore can take care of the navigation. + +```dart title=lib/pages/login_page.dart +import 'package:flutter/material.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({Key? key}) : super(key: key); + + static Route route() { + return MaterialPageRoute( + builder: (context) => const LoginPage()); + } + + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + bool _isLoading = false; + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + Future _signIn() async { + setState(() { + _isLoading = true; + }); + try { + await supabase.auth.signInWithPassword( + email: _emailController.text, + password: _passwordController.text, + ); + } on AuthException catch (error) { + context.showErrorSnackBar(message: error.message); + } catch (_) { + context.showErrorSnackBar( + message: unexpectedErrorMessage); + } + if (mounted) { + setState(() { + _isLoading = true; + }); + } + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Sign In')), + body: ListView( + padding: formPadding, + children: [ + TextFormField( + controller: _emailController, + decoration: + const InputDecoration(labelText: 'Email'), + keyboardType: TextInputType.emailAddress, + ), + spacer, + TextFormField( + controller: _passwordController, + decoration: const InputDecoration( + labelText: 'Password'), + obscureText: true, + ), + spacer, + ElevatedButton( + onPressed: _isLoading ? null : _signIn, + child: const Text('Login'), + ), + ], + ), + ); + } +} +``` + +Notice that I have not used bloc anywhere on the register or login page. +I try to only use state management libraries for pages that have some complexity. +Since both register and login pages are relatively simple, I am going with the good old `setState`. + +We should also modify the splash page to redirect signed-in users to the RoomsPage by default. + +```dart title=lib/pages/splash_page.dart +import 'package:flutter/material.dart'; +import 'package:my_chat_app/pages/register_page.dart'; +import 'package:my_chat_app/pages/rooms_page.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// Page to redirect users to the appropreate page depending on the initial auth state +class SplashPage extends StatefulWidget { + const SplashPage({Key? key}) : super(key: key); + + @override + SplashPageState createState() => SplashPageState(); +} + +class SplashPageState extends State { + @override + void initState() { + getInitialSession(); + super.initState(); + } + + Future getInitialSession() async { + // quick and dirty way to wait for the widget to mount + await Future.delayed(Duration.zero); + + try { + final session = + await SupabaseAuth.instance.initialSession; + if (session == null) { + Navigator.of(context).pushAndRemoveUntil( + RegisterPage.route(), (_) => false); + } else { + Navigator.of(context).pushAndRemoveUntil( + RoomsPage.route(), (_) => false); + } + } catch (_) { + context.showErrorSnackBar( + message: 'Error occured during session refresh', + ); + Navigator.of(context).pushAndRemoveUntil( + RegisterPage.route(), (_) => false); + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } +} +``` + +Finally we implement those `ProfilesCubit` that you saw here and there throughout the code. +This cubit will act as in memory cache of all the profiles data so that the app does not have to go fetch the same profiles every single time it needs it. +Create `profiles_state.dart` and `profiles_cubit.dart` under `lib/cubits/` and add the following code. + +```dart title=lib/cubits/profiles_state.dart +part of 'profiles_cubit.dart'; + +@immutable +abstract class ProfilesState {} + +class ProfilesInitial extends ProfilesState {} + +class ProfilesLoaded extends ProfilesState { + ProfilesLoaded({ + required this.profiles, + }); + + final Map profiles; +} +``` + +```dart title=lib/cubits/profiles_cubit.dart +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:my_chat_app/models/profile.dart'; +import 'package:my_chat_app/utils/constants.dart'; + +part 'profiles_state.dart'; + +class ProfilesCubit extends Cubit { + ProfilesCubit() : super(ProfilesInitial()); + + /// Map of app users cache in memory with profile_id as the key + final Map _profiles = {}; + + Future getProfile(String userId) async { + if (_profiles[userId] != null) { + return; + } + + final data = await supabase + .from('profiles') + .select() + .match({'id': userId}).single(); + + if (data == null) { + return; + } + _profiles[userId] = Profile.fromMap(data); + + emit(ProfilesLoaded(profiles: _profiles)); + } +} +``` + +We will make the `ProfilesCubit` accessible from anywhere in the app with the following code in `main.dart` file. + +```dart title=lib/main.dart +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_chat_app/cubits/profiles/profiles_cubit.dart'; +import 'package:my_chat_app/utils/constants.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:my_chat_app/pages/splash_page.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await Supabase.initialize( + // TODO: Replace credentials with your own + url: 'supabase_url', + anonKey: 'supabase_anon_key', + authCallbackUrlHostname: 'login', + ); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ProfilesCubit(), + child: MaterialApp( + title: 'SupaChat', + debugShowCheckedModeBanner: false, + theme: appTheme, + home: const SplashPage(), + ), + ); + } +} +``` + +### Step 4: Authorization with Row Level Security (RLS) + +At this point, we seemingly have a complete app, except if we open the app right now, we will see every user’s room with all the messages that have ever been sent within the app by any user. This is because we have not set up Row Level Security yet. [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security) is a mechanism in PostgreSQL that allows developers to define who can perform what operation on a row-by-row basis. There are two ways we can define Row Level Security policies in Supabase, with the GUI or through SQL, but we will set them using SQL today. Let’s run the following SQL to set the security policy. + +```sql +-- Returns true if the signed in user is a participant of the room +create or replace function is_room_participant(room_id uuid) +returns boolean as $$ + select exists( + select 1 + from room_participants + where room_id = is_room_participant.room_id and profile_id = auth.uid() + ); +$$ language sql security definer; + + +-- *** Row level security polities *** + + +alter table public.profiles enable row level security; +create policy "Public profiles are viewable by everyone." + on public.profiles for select using (true); + + +alter table public.rooms enable row level security; +create policy "Users can view rooms that they have joined" + on public.rooms for select using (is_room_participant(id)); + + +alter table public.room_participants enable row level security; +create policy "Participants of the room can view other participants." + on public.room_participants for select using (is_room_participant(room_id)); + + +alter table public.messages enable row level security; +create policy "Users can view messages on rooms they are in." + on public.messages for select using (is_room_participant(room_id)); +create policy "Users can insert messages on rooms they are in." + on public.messages for insert with check (is_room_participant(room_id) and profile_id = auth.uid()); +``` + +Notice that we have created a handy `is_room_participant` function that will return whether a particular user is a participant or not in a specific room. + +With the Row Level Security policies set up, our application is complete. We now have a real-time chat application with proper authentication and authorization in place. + +## Conclusions/ Future Improvements + +Continuing from our [previous article](https://supabase.com/blog/flutter-tutorial-building-a-chat-app), we added proper authorization to our chat application using Row Level Security, which enabled us to add 1 on 1 chat feature. +We used bloc for our state management solution. One thing we could have done differently if we were to write test codes was to pass the supabase instance as a parameter of the cubit so that we could [write tests using the bloc_test package](https://bloclibrary.dev/#/testing). + +We could also explore some cool feature improvement. +At the top of the rooms page, we are loading the newest created users to start a conversation. This is fine, but it only allows users to start a conversation with new users. +We can for example update this to a list of users that are online at the same time. We can implement this using the [presence feature](https://supabase.com/docs/guides/realtime#presence) of Supabase. + +## More Flutter Resources + +- [Complete set of code of this chat app](https://github.com/supabase-community/flutter-chat/tree/with_auth) +- [supabase-flutter docs](https://supabase.com/docs/reference/dart) +- [Flutter Supabase Quick Starter Guide](https://supabase.com/docs/guides/with-flutter) +- [supabase-flutter v1.0 released](https://supabase.com/blog/supabase-flutter-sdk-v1-released) +- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase) diff --git a/apps/www/public/images/blog/flutter-chat-auth/chat-app-screenshot.png b/apps/www/public/images/blog/flutter-chat-auth/chat-app-screenshot.png new file mode 100644 index 00000000000..d6542764ac6 Binary files /dev/null and b/apps/www/public/images/blog/flutter-chat-auth/chat-app-screenshot.png differ diff --git a/apps/www/public/images/blog/flutter-chat-auth/rooms-and-chat-page.png b/apps/www/public/images/blog/flutter-chat-auth/rooms-and-chat-page.png new file mode 100644 index 00000000000..557326c3ef7 Binary files /dev/null and b/apps/www/public/images/blog/flutter-chat-auth/rooms-and-chat-page.png differ diff --git a/apps/www/public/images/blog/flutter-chat-auth/supabase-flutter-auth.jpeg b/apps/www/public/images/blog/flutter-chat-auth/supabase-flutter-auth.jpeg new file mode 100644 index 00000000000..bef7e32f2a8 Binary files /dev/null and b/apps/www/public/images/blog/flutter-chat-auth/supabase-flutter-auth.jpeg differ diff --git a/apps/www/public/images/blog/flutter-chat-auth/url-configuration.png b/apps/www/public/images/blog/flutter-chat-auth/url-configuration.png new file mode 100644 index 00000000000..1b992b67a50 Binary files /dev/null and b/apps/www/public/images/blog/flutter-chat-auth/url-configuration.png differ diff --git a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts index 65ded062114..01e03605441 100644 --- a/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts +++ b/examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts @@ -3,7 +3,7 @@ // This enables autocomplete, go to definition, etc. import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "get-tshirt-competition" up and running!`) diff --git a/examples/edge-functions/supabase/functions/read-storage/index.ts b/examples/edge-functions/supabase/functions/read-storage/index.ts index b1fbbf96ba3..d7247b800dc 100644 --- a/examples/edge-functions/supabase/functions/read-storage/index.ts +++ b/examples/edge-functions/supabase/functions/read-storage/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from "https://deno.land/std@0.131.0/http/server.ts" -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0' +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -29,14 +29,11 @@ serve(async (req) => { { global: { headers: { Authorization: req.headers.get('Authorization')! } } } ) - const { data, error } = await supabaseClient - .storage - .from('my-bucket') - .download('sample.txt') + const { data, error } = await supabaseClient.storage.from('my-bucket').download('sample.txt') if (error) throw error // file contents are returned as a blob, we can convert it to utf-8 text by calling text() method. - const contents = await data.text(); + const contents = await data.text() // prints out the contents of the file console.log(contents) @@ -45,14 +42,12 @@ serve(async (req) => { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, }) - } catch (error) { - console.error(error); + console.error(error) return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, }) } - }) diff --git a/examples/edge-functions/supabase/functions/restful-tasks/index.ts b/examples/edge-functions/supabase/functions/restful-tasks/index.ts index 7451c623890..67c36ed0a98 100644 --- a/examples/edge-functions/supabase/functions/restful-tasks/index.ts +++ b/examples/edge-functions/supabase/functions/restful-tasks/index.ts @@ -2,8 +2,8 @@ // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. -import { serve } from "https://deno.land/std@0.131.0/http/server.ts" -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0' +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import { createClient, SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -11,58 +11,58 @@ const corsHeaders = { } interface Task { - name: string; - status: number; + name: string + status: number } -async function getTask(supabaseClient: any, id: string) { - const { data: task, error } = await supabaseClient.from('tasks').select('*').eq('id', id); +async function getTask(supabaseClient: SupabaseClient, id: string) { + const { data: task, error } = await supabaseClient.from('tasks').select('*').eq('id', id) if (error) throw error return new Response(JSON.stringify({ task }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, - }); + }) } -async function getAllTasks(supabaseClient: any) { +async function getAllTasks(supabaseClient: SupabaseClient) { const { data: tasks, error } = await supabaseClient.from('tasks').select('*') if (error) throw error return new Response(JSON.stringify({ tasks }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, - }); + }) } -async function deleteTask(supabaseClient: any, id: string) { - const { error } = await supabaseClient.from('tasks').delete().eq('id', id); +async function deleteTask(supabaseClient: SupabaseClient, id: string) { + const { error } = await supabaseClient.from('tasks').delete().eq('id', id) if (error) throw error return new Response(JSON.stringify({}), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, - }); + }) } -async function updateTask(supabaseClient: any, id: string, task: Task) { - const { error } = await supabaseClient.from('tasks').update(task).eq('id', id); +async function updateTask(supabaseClient: SupabaseClient, id: string, task: Task) { + const { error } = await supabaseClient.from('tasks').update(task).eq('id', id) if (error) throw error return new Response(JSON.stringify({ task }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, - }); + }) } -async function createTask(supabaseClient: any, task: Task) { - const { error } = await supabaseClient.from('tasks').insert(task); +async function createTask(supabaseClient: SupabaseClient, task: Task) { + const { error } = await supabaseClient.from('tasks').insert(task) if (error) throw error return new Response(JSON.stringify({ task }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, - }); + }) } serve(async (req) => { @@ -90,10 +90,10 @@ serve(async (req) => { const matchingPath = taskPattern.exec(url) const id = matchingPath ? matchingPath.pathname.groups.id : null - let task = null; - if (method === "POST" || method === "PUT") { - const body = await req.json(); - task = body.task; + let task = null + if (method === 'POST' || method === 'PUT') { + const body = await req.json() + task = body.task } // call relevant method based on method and id @@ -112,7 +112,7 @@ serve(async (req) => { return getAllTasks(supabaseClient) } } catch (error) { - console.error(error); + console.error(error) return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, diff --git a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts index eb74cca8c38..f9dedb16a7d 100644 --- a/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts +++ b/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts @@ -3,7 +3,7 @@ // This enables autocomplete, go to definition, etc. import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' -import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.0.0' +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { corsHeaders } from '../_shared/cors.ts' console.log(`Function "select-from-table-with-auth-rls" up and running!`) diff --git a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index e7707fddb08..c438d96b468 100644 --- a/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -46,7 +46,7 @@ const ProviderForm: FC = ({ provider }) => { const { error } = await authConfig.update(payload) if (!error) { - resetForm({ values: payload, initialValues: payload }) + resetForm({ values: { ...values }, initialValues: { ...values } }) setOpen(false) ui.setNotification({ category: 'success', message: 'Successfully updated settings' }) } else { diff --git a/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx b/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx index 5f35d71f5f7..cf97a169668 100644 --- a/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx +++ b/studio/components/interfaces/Billing/AddOns/PITRDurationSelection.tsx @@ -29,7 +29,7 @@ const PITRDurationSelection: FC = ({ const addonUpdateDisabled = useFlag('disableProjectCreationAndUpdate') // Only projects of version greater than supabase-postgrest-14.1.0.44 can use PITR - const canUsePITR = getSemanticVersion(ui.selectedProject?.kpsVersion ?? '') > 141044 + const canUsePITR = getSemanticVersion(ui.selectedProject?.dbVersion ?? '') > 141044 return (
diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx index 6f53bc8760d..37f77b19b67 100644 --- a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentSummaryPanel.tsx @@ -1,6 +1,6 @@ import { FC, useState } from 'react' -import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui' import * as Tooltip from '@radix-ui/react-tooltip' +import { Listbox, IconLoader, Button, IconPlus, IconAlertCircle, IconCreditCard } from 'ui' import { PermissionAction } from '@supabase/shared-types/out/constants' import { checkPermissions, useFlag, useStore } from 'hooks' @@ -319,16 +319,54 @@ const PaymentSummaryPanel: FC = ({
- + + + + + {!hasChangesToPlan ? ( + + +
+ + No changes made to your subscription + +
+
+ ) : !selectedPaymentMethod ? ( + + +
+ Please select a payment method +
+
+ ) : ( + <> + )} +
diff --git a/studio/types/base.ts b/studio/types/base.ts index 002e303ee2c..c86d136afe7 100644 --- a/studio/types/base.ts +++ b/studio/types/base.ts @@ -25,6 +25,7 @@ export interface ProjectBase { export interface Project extends ProjectBase { // available after projects.fetchDetail connectionString?: string + dbVersion?: string kpsVersion?: string restUrl?: string // store subscription tier products.metadata.supabase_prod_id