diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml new file mode 100644 index 00000000000..bd402e9f365 --- /dev/null +++ b/.github/workflows/docs-tests.yml @@ -0,0 +1,30 @@ +name: Docs Tests + +on: + pull_request: + branches: ['master'] + paths: + - 'apps/docs/**/*.{ts,tsx}' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install deps + run: npm ci + + - name: Run tests + run: npm run test:docs diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts new file mode 100644 index 00000000000..2d2d1285229 --- /dev/null +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLELements.utils.test.ts @@ -0,0 +1,96 @@ +import { getAnchor, removeAnchor } from './CustomHTMLElements.utils' + +describe('CustomHTMLElementsUtils', () => { + describe('getAnchor', () => { + describe('when value is an object', () => { + it('returns slugified version of props.children', () => { + const value = { + props: { + children: 'Full Text Search', + }, + } + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + }) + + describe('when value is an array', () => { + describe('when custom anchor exists', () => { + it('returns inner slug', () => { + const value = ['Proximity: <->', '[#proximity]'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + + it('trims whitespace', () => { + const value = ['Proximity: <->', ' [#proximity] '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('proximity') + }) + }) + + it('returns concatenated slug of elements', () => { + const value = ['Full', 'Text', 'Search'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('trims whitespace', () => { + const value = [' Full ', ' Text ', ' Search '] + + const result = getAnchor(value) + + expect(result).toStrictEqual('full-text-search') + }) + + it('removes special characters', () => { + const value = ['function()'] + + const result = getAnchor(value) + + expect(result).toStrictEqual('function') + }) + }) + + describe('when value is a string', () => { + it('returns slugified version of string', () => { + const value = 'My (Very) Awesome Heading' + + const result = getAnchor(value) + + expect(result).toStrictEqual('my-very-awesome-heading') + }) + }) + }) + + describe('removeAnchor', () => { + describe('when value is an array', () => { + it('filters out custom anchor elements', () => { + const value = ['My (Very) Awesome Heading', '[#my-custom-heading]'] + + const result = removeAnchor(value) + + expect(result).toStrictEqual(['My (Very) Awesome Heading']) + }) + }) + + describe('when value is a string', () => { + it('strips out custom anchor string', () => { + const value = 'My (Very) Awesome Heading [#my-custom-heading]' + + const result = removeAnchor(value) + + // Original implementation didn't trim the resulting string - not sure if it really matters + expect(result).toStrictEqual('My (Very) Awesome Heading ') + }) + }) + }) +}) diff --git a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts index 0103cef6aa7..844724a5466 100644 --- a/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts +++ b/apps/docs/components/CustomHTMLElements/CustomHTMLElements.utils.ts @@ -2,53 +2,60 @@ 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(']') - ) - if (customAnchor !== undefined) return customAnchor.slice(2, customAnchor.indexOf(']')) + const customAnchor = text.find((x) => typeof x === 'string' && hasCustomAnchor(x)) + if (customAnchor !== undefined) { + return parseCustomAnchor(customAnchor) + } const formattedText = text .map((x) => { - if (typeof x !== 'string') return x.props.children - else return x.trim() + if (typeof x !== 'string') { + return x.props.children + } + + return x.trim() }) .map((x) => { - if (typeof x !== 'string') return x - else + if (typeof x !== 'string') { return x - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + } + + return slugify(x) }) return formattedText.join('-').toLowerCase() } else { const anchor = text.props.children if (typeof anchor === 'string') { - return anchor - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + return slugify(anchor) } return anchor } } else if (typeof text === 'string') { - if (text.includes('[#') && text.endsWith(']')) { - return text.slice(text.indexOf('[#') + 2, text.indexOf(']')) - } else { - return text - .toLowerCase() - .replace(/[^a-z0-9- ]/g, '') - .replace(/[ ]/g, '-') + if (hasCustomAnchor(text)) { + return parseCustomAnchor(text) } + return slugify(text) } else { return undefined } } +const hasCustomAnchor = (value: string): boolean => value.includes('[#') && value.includes(']') + +const parseCustomAnchor = (value: string): string => + value.slice(value.indexOf('[#') + 2, value.indexOf(']')) + +const slugify = (value: string): string => + value + .toLowerCase() + .trim() + .replace(/[^a-z0-9- ]/g, '') + .replace(/[ ]/g, '-') + 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' && hasCustomAnchor(x))) } else if (typeof text === 'string') { if (text.indexOf('[#') > 0) return text.slice(0, text.indexOf('[#')) else return text diff --git a/apps/docs/components/Navigation/Navigation.types.ts b/apps/docs/components/Navigation/Navigation.types.ts index a644cc7c70d..3882fb26e77 100644 --- a/apps/docs/components/Navigation/Navigation.types.ts +++ b/apps/docs/components/Navigation/Navigation.types.ts @@ -9,8 +9,8 @@ export interface NavMenuGroup { export interface NavMenuSection { name: string - url?: string - items: NavMenuSection[] + url?: `/${string}` + items: Partial[] } export interface References { @@ -26,10 +26,17 @@ export interface References { type MenuItem = { label: string icon?: string - href?: string + href?: `/${string}` | `https://${string}` level?: string hasLightIcon?: boolean community?: boolean } export type HomepageMenuItems = MenuItem[][] + +export type NavMenuConstant = Readonly<{ + title: string + icon: string + url?: `/${string}` + items: ReadonlyArray> +}> diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index da8dae4bbbc..64a9995d9f9 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1,4 +1,4 @@ -import { References, HomepageMenuItems } from '../Navigation.types' +import type { HomepageMenuItems, NavMenuConstant, References } from '../Navigation.types' export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [ [ @@ -194,7 +194,7 @@ export const REFERENCES: References = { }, } -export const gettingstarted = { +export const gettingstarted: NavMenuConstant = { icon: 'getting-started', title: 'Getting Started', items: [ @@ -321,7 +321,7 @@ export const SocialLoginItems = [ url: '/guides/auth/social-login/auth-apple', }, { - name: 'Azure', + name: 'Azure (Microsoft)', icon: '/docs/img/icons/microsoft-icon', url: '/guides/auth/social-login/auth-azure', }, @@ -507,7 +507,7 @@ export const auth = { ], } -export const database = { +export const database: NavMenuConstant = { icon: 'database', title: 'Database', url: '/guides/database', @@ -518,6 +518,8 @@ export const database = { { name: 'Database Webhooks', url: '/guides/database/webhooks' }, { name: 'Full Text Search', url: '/guides/database/full-text-search' }, { name: 'Database Testing', url: '/guides/database/testing' }, + { name: 'Managing Secrets with Vault', url: '/guides/database/vault' }, + { name: 'Column Encryption', url: '/guides/database/column-encryption' }, { name: 'Extensions', url: undefined, @@ -662,7 +664,7 @@ export const database = { ], } -export const api = { +export const api: NavMenuConstant = { icon: 'serverless-apis', title: 'Serverless APIs', url: '/guides/api', @@ -700,7 +702,7 @@ export const api = { ], } -export const functions = { +export const functions: NavMenuConstant = { icon: 'edge-functions', title: 'Edge Functions', url: '/guides/functions', @@ -778,7 +780,7 @@ export const functions = { ], } -export const realtime = { +export const realtime: NavMenuConstant = { icon: 'realtime', title: 'Realtime', url: '/guides/realtime', @@ -841,7 +843,7 @@ export const realtime = { ], } -export const storage = { +export const storage: NavMenuConstant = { icon: 'storage', title: 'Storage', url: '/guides/storage', @@ -855,7 +857,7 @@ export const storage = { ], } -export const supabase_cli = { +export const supabase_cli: NavMenuConstant = { icon: 'reference-cli', title: 'Supabase CLI', url: '/guides/cli', @@ -877,7 +879,7 @@ export const supabase_cli = { ], } -export const platform = { +export const platform: NavMenuConstant = { icon: 'platform', title: 'Platform', url: '/guides/platform', @@ -963,7 +965,7 @@ export const platform = { ], } -export const resources = { +export const resources: NavMenuConstant = { icon: 'resources', title: 'Resources', url: '/guides/resources', @@ -1004,7 +1006,7 @@ export const resources = { ], } -export const self_hosting = { +export const self_hosting: NavMenuConstant = { title: 'Self-Hosting', icon: 'self-hosting', url: '/guides/self-hosting', @@ -1061,7 +1063,7 @@ export const migrate = { ], } -export const integrations = { +export const integrations: NavMenuConstant = { icon: 'integrations', title: 'Integrations', url: '/guides/integrations', @@ -1092,6 +1094,7 @@ export const integrations = { name: 'Developer Tools', url: undefined, items: [ + { name: 'Cloudflare Workers', url: '/guides/integrations/cloudflare-workers' }, { name: 'Estuary', url: '/guides/integrations/estuary' }, { name: 'OpenAI', url: '/guides/functions/examples/openai' }, { name: 'pgMustard', url: '/guides/integrations/pgmustard' }, diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 2f683b429f7..f264a4112b7 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -46,7 +46,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any if (activeItem && activeItemRef.current) { // this is a hack, but seems a common one on Stackoverflow setTimeout(() => { - activeItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + activeItemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) }, 0) } }) diff --git a/apps/docs/data/authProviders.ts b/apps/docs/data/authProviders.ts index ea5855061bd..a1053a30772 100644 --- a/apps/docs/data/authProviders.ts +++ b/apps/docs/data/authProviders.ts @@ -10,7 +10,7 @@ const authProviders = [ authType: 'social', }, { - name: 'Azure', + name: 'Azure (Microsoft)', logo: '/docs/img/icons/microsoft-icon', href: '/guides/auth/social-login/auth-azure', official: false, diff --git a/apps/docs/jest.config.ts b/apps/docs/jest.config.ts new file mode 100644 index 00000000000..348b75c0bb7 --- /dev/null +++ b/apps/docs/jest.config.ts @@ -0,0 +1,9 @@ +import type { Config } from '@jest/types' + +const config: Config.InitialOptions = { + preset: 'ts-jest', + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], + testEnvironment: 'jsdom', +} + +export default config diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index d33090df77e..35c269c1b4b 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -62,6 +62,7 @@ const nextConfig = { 'raw.githubusercontent.com', 'weweb-changelog.ghost.io', 'img.youtube.com', + 'archbee-image-uploads.s3.amazonaws.com' ], }, experimental: { diff --git a/apps/docs/package.json b/apps/docs/package.json index 28ce0d084d8..c59b1dcf3c4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,6 +9,7 @@ "build:analyze": "ANALYZE=true next build", "start": "next start", "lint": "next lint", + "test": "jest", "build:sitemap": "node ./internals/generate-sitemap.mjs", "embeddings": "tsx scripts/search/generate-embeddings.ts", "embeddings:refresh": "npm run embeddings -- --refresh", diff --git a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx b/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx index be152b784ce..f16188a6bc8 100644 --- a/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx +++ b/apps/docs/pages/guides/auth/auth-helpers/nextjs-server-components.mdx @@ -249,6 +249,8 @@ export const useSupabase = () => { Modify `layout.jsx` to wrap the application with the `` component: ```jsx title="app/layout.jsx" +import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { headers, cookies } from 'next/headers' import './globals.css' import SupabaseProvider from './supabase-provider' @@ -257,14 +259,20 @@ export const metadata = { description: 'Generated by create next app', } -export default function RootLayout({ children }) { +export default async function RootLayout({ children }) { + const supabase = createServerComponentSupabaseClient({ + headers, + cookies, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + return ( - - - {children} - + {children} ) @@ -278,6 +286,8 @@ export default function RootLayout({ children }) { Modify `layout.tsx` to wrap the application with the `` component: ```tsx title="app/layout.tsx" +import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs' +import { headers, cookies } from 'next/headers' import './globals.css' import SupabaseProvider from './supabase-provider' @@ -286,14 +296,20 @@ export const metadata = { description: 'Generated by create next app', } -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const supabase = createServerComponentSupabaseClient({ + headers, + cookies, + }) + + const { + data: { session }, + } = await supabase.auth.getSession() + return ( - - - {children} - + {children} ) diff --git a/apps/docs/pages/guides/auth/row-level-security.mdx b/apps/docs/pages/guides/auth/row-level-security.mdx index 1987e8acbea..94d2e430d09 100644 --- a/apps/docs/pages/guides/auth/row-level-security.mdx +++ b/apps/docs/pages/guides/auth/row-level-security.mdx @@ -286,43 +286,24 @@ using ( ## Tips -### Enable Realtime for database tables - -Realtime server broadcasts database changes to authorized users depending on your Row Level Security (RLS) policies. -We recommend that you enable row level security and set row security policies on tables that you add to the publication. -However, you may choose to disable RLS on a table and have changes broadcast to all connected clients. - -```sql -/** - * REALTIME SUBSCRIPTIONS - * Realtime enables listening to any table in your public schema. - */ - -begin; - -- remove the realtime publication - drop publication if exists supabase_realtime; - - -- re-create the publication but don't enable it for any tables - create publication supabase_realtime; -commit; - --- add a table to the publication -alter publication supabase_realtime add table products; - --- add other tables to the publication -alter publication supabase_realtime add table posts; -``` - ### You don't have to use policies -You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. +You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel. -Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all. +Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser. -However, if you want to use another authorization method for your applications, that's also fine. Supabase is "just Postgres", so if your application -works with Postgres, then it also works with Supabase. +That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase. -Tip: Make sure to enable RLS for all your tables, so that your tables are inaccessible. Then use the "Service" which we provide, which is designed to bypass RLS. +If you plan to use this approach make sure to enable RLS for your tables. Then use the `service_role` key (for our client libraries) or the `postgres` role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient: + +```sql +create table profiles ( + id serial primary key, + email text +); + +alter table profiles enable row level security; +``` ### Never use a service key on the client diff --git a/apps/docs/pages/guides/auth/server-side-rendering.mdx b/apps/docs/pages/guides/auth/server-side-rendering.mdx index 7b7edb00924..9bcd4a75e41 100644 --- a/apps/docs/pages/guides/auth/server-side-rendering.mdx +++ b/apps/docs/pages/guides/auth/server-side-rendering.mdx @@ -306,3 +306,7 @@ export const Page = ({ children }) => export default Page +### Which authentication flows have PKCE support? + +At present, PKCE is supported on the Magic Link, OAuth, Sign Up, and Password Recovery routes. These correspond to the `signInWithOtp`, `signInWithOAuth`, `signUp`, and `resetPasswordForEmail` methods on the Supabase client library. When using PKCE with Phone and Email OTPs, there is no behavior change with respect to the implicit flow - an access token will be returned in the body when a request is successful. + diff --git a/apps/docs/pages/guides/auth/social-login/auth-azure.mdx b/apps/docs/pages/guides/auth/social-login/auth-azure.mdx index d2ee9f865f7..bf2e2ed469f 100644 --- a/apps/docs/pages/guides/auth/social-login/auth-azure.mdx +++ b/apps/docs/pages/guides/auth/social-login/auth-azure.mdx @@ -2,11 +2,11 @@ import Layout from '~/layouts/DefaultGuideLayout' export const meta = { id: 'auth-azure', - title: 'Login with Azure', - description: 'Add Azure OAuth to your Supabase project', + title: 'Login with Azure (Microsoft)', + description: 'Add Azure (Microsoft) OAuth to your Supabase project', } -To enable Azure Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard. +To enable Azure (Microsoft) Auth for your project, you need to set up an Azure OAuth application and add the application credentials to your Supabase Dashboard. ## Overview diff --git a/apps/docs/pages/guides/database/column-encryption.mdx b/apps/docs/pages/guides/database/column-encryption.mdx new file mode 100644 index 00000000000..80ed59f1705 --- /dev/null +++ b/apps/docs/pages/guides/database/column-encryption.mdx @@ -0,0 +1,249 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'column-encryption', + title: 'Column Encryption', + description: 'Use Supabase to store and serve files.', + sidebar_label: 'Overview', + video: 'https://www.youtube.com/v/J9mTPY8rIXE', +} + +Encrypted Columns for Tables + +## Transparent Column Encryption (TCE) + +TCE provides a safe way to encrypt your data so that it doesn't leak into logs and backups. It can also provide row-level authenticated encryption. + +TCE is the primary building block of [Vault](/docs/guides/database/vault), Supabase's Postgres secrets manager. Vault is a built-in table with an integrated UI intended to act as a secure global secrets management for you project. However if you need more fine-grain control over your encrypted data, such as encrypting columns in your own tables, you can use TCE directly. Any Postgres value that can be cast to `text` or `bytea` can be encrypted using TCE. + +### Encrypting columns + +When creating a new column in the Dashboard, you can choose to encrypt a `text` or `bytea` column. You will choose which key you would like to encrypt it with by selecting an existing key ID or creating a new one. + +![Encrypting columns](/docs/img/guides/database/vault-encrypting-columns.png) + +Once you've created an encrypted column, you can insert data into the table like you would any other table. For example if you were to insert an email address into an encrypted column, you will see that the address is transparently converted into an encrypted value on the new row. + +![Encrypted data](/docs/img/guides/database/vault-encrypted-data.png) + +Decrypted data is accessed using a special view that is automatically created after adding an encrypted column to a table. This view decrypts the data row-by-row as you access it. By default, this view is called `decrypted_`. In the example below, the decryption view for the `profiles` table is called `decrypted_profiles`. Notice there is a new column in the view called `decrypted_emails` that contains the decrypted email value. + +![Decrypted data](/docs/img/guides/database/vault-decrypted-data.png) + +## How Key Derivation Works + +The current state-of-the-art in encryption libraries is [libsodium](https://doc.libsodium.org/). + +**libsodium** offers a range of APIs for authenticated secret and public key encryption, key derivation, encrypted streaming, [AEAD](https://en.wikipedia.org/wiki/Authenticated_encryption), various forms of hashing, and much more. + +This powerful API is available to PostgreSQL using the [**pgsodium**](https://github.com/michelp/pgsodium) extension. **pgsodium** provides all the functionality of the full **libsodium** API, but previously it required developers to set up database encryption themselves, which remained a challenge even for those familiar with database administration. + +To solve this problem, **pgsodium** now has a full key management API, primarily via the table `pgsodium.key` and the `pgsodium.create_key()` function. This key table contains no raw keys, but instead uses libsodium Key IDs to derive keys that are used internally for encryption. A [key derivation function](https://libsodium.gitbook.io/doc/key_derivation) is used with an internal root key that is unavailable to SQL and not stored in the database, but rather managed by you externally using flexible scripts, or by Supabase automatically as part of our service offering. + +The simplest way to use **pgsodium** to encrypt and decrypt data is to first create a **Key ID**. Valid Key IDs are stored in pgsodium in a special extension table, and they can be created using the `pgsodium.create_key()` function. This function takes a number of arguments depending on how it's used, but the simplest case is to create a new key with no arguments: + +```sql +select * from pgsodium.create_key(); +-[ RECORD 1 ]---+------------------------------------- +id | eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d +name | +status | valid +key_type | aead-det +key_id | 2 +key_context | \x7067736f6469756d +created | 2022-11-13 21:19:35.765823+00 +expires | +associated_data | +``` + +This key can now be used with pgsodium encryption functions by its UUID (`eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d`). For example: + +```sql +select * from pgsodium.crypto_aead_det_encrypt ( + 'this is the message', -- a message to encrypt + 'this is associated data', -- some authenticated associated data + 'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid -- key ID +); +``` + +This produces the following encrypted “ciphertext” using the `aead-det` algorithm from the libsodium [XChaCha20-SIV](https://github.com/jedisct1/libsodium-xchacha20-siv) encryption function. + +```sql +-[ RECORD 1 ]-----------+--------------------------------------------------------------------------------------------------------- +crypto_aead_det_encrypt | \\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742 +``` + +Now to decrypt the ciphertext, pass it to the decryption function _with the same Key ID_: + +```sql +select * +from convert_from(pgsodium.crypto_aead_det_decrypt ( + '\\x099baa820250d7375ed141f8f1936af384bc229f3de1010a6eff6ffdaf3998baffbae75b5cd83d1c469407ff2d3764a428b742', + 'this is associated data', + 'eaa20d8c-c77c-4985-9f73-2a5f5d1f1e6d'::uuid +), 'utf8'); +``` + +Which recovers the original “plaintext” message: + +```sql +-[ RECORD 1 ]+-------------------- +convert_from | this is the message +``` + +In the above example, there is _no raw key_, only a Key ID which is used to derive the key used to encrypt the message and authenticate it with the associated data. In fact, it is impossible for a SQL user to derive the key used above, and if the Key ID is stored, then no encryption keys or decrypted information will leak into backups, disk storage, or the database WAL stream. + +## Transparent Column Encryption + +As of **pgsodium** 3.0.0 and up, the extension offers a simple and declarative Transparent Column Encryption feature (TCE). This feature is now shipped with all Supabase projects. TCE allows you to specify encrypted columns within a table and generates a new view that “wraps” that table to decrypt the contents. + +TCE works using two dynamically generated objects for tables that contain encrypted columns: + +- an `INSERT UPDATE` trigger that encrypts data when it is inserted or modified +- a view that is created to wrap the table to decrypt the data when it is accessed + +To “transparently” decrypt the table, access the dynamically generated view _instead of the table_. For every encrypted column in the table, the view will have an additional decrypted column that shows the decrypted result. + +It's worth noting at this point that sometimes there is some confusion about handling encrypted data with TCE. The `T` stands for **Transparent** which means, you can always see decrypted data through the view, where the decrypted data can't be see is when stored on disk, or in pg_dumps, backups, WAL streams, etc. This is often called **Encryption At Rest** and is one layer in many that may be used to encrypt and protect your data. + +Often Transparent encryption is understood to be “Transparent Disk Encryption” or “Full Disk Encryption”, this is where a drive is encrypted but reading and writing that drive is decrypted. TCE is similar to this, where data on disk is encrypted, but it is more fine grained, only particular columns are encrypted. The data is also encrypted in the sense that the table stored on disk contains encrypted data, without the view or the key, pg_dumps and backups still contain encrypted data, this is not possible with disk-only encryption. + +For the moment TCE only works for columns of type `text` (or types castable to `text` like `json`). Soon we will also support `bytea` and possibly more as use cases and tests get better. + +TCE uses one of PostgreSQL's lesser-known features: [`SECURITY LABEL`](https://www.postgresql.org/docs/current/sql-security-label.html). A security label can be thought of as a simple label which is attached to an object (a table, column, etc). Each label is scoped to an extension and that extension can provide security features depending on the label. + +Let's see a simple example of using `SECURITY LABEL` to encrypt a column using **pgsodium**. + +### One Key ID for the Entire Column + +For the simplest case, a column can be encrypted with one Key ID which must be of the type `aead-det` (as created above): + +```sql +create table secrets ( + id bigserial primary key, + secret_number text +); + +security label for pgsodium + on column secrets.secret_number + is 'ENCRYPT WITH KEY ID e348034b-3f07-4878-aad6-000511d12826'; +``` + +The advantage of this approach is simplicity - the user creates one key and labels a column with it. The cryptographic algorithm for this approach uses a _nonceless_ encryption algorithm called `crypto_aead_det_xchacha20()`. This algorithm is written by the author of libsodium and can be found [here](https://github.com/jedisct1/libsodium-xchacha20-siv). + +Using one key for an entire column means that whoever can decrypt one row can decrypt them all from a database dump. Also changing (rotating) the key means rewriting the whole table. + +### One Key ID per Row + +A more fine grained approach would be storing one Key ID per row: + +```sql +create table secrets ( + id bigserial primary key, + secret text, + key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid +); + +security label for pgsodium + on column secrets.secret + is 'ENCRYPT WITH KEY COLUMN key_id'; +``` + +This approach ensures that a key for one user doesn't necessarily decrypt any others. While rows can share key IDs, they don't necessarily have to (unlike the first example above where one key id was used for the entire column). It also acts as a natural partition that can work in conjunction with Row Level Security to share distinct keys between owners. + +Notice also how there is a `DEFAULT` value for the `key_id`. In a way, this gives you the best of both approaches - encrypting the column when a per-row key ID is not provided. The downside to this approach is that you need to store one key ID per row, which takes up more disk space (but that's cheap!). + +### One Key ID per Row with Nonce Support + +The default cryptographic algorithm for the above approach uses a _nonceless_ encryption algorithm called [`crypto_aead_det_xchacha20()`](https://github.com/jedisct1/libsodium-xchacha20-siv). This algorithm has the advantage that it does not require nonce values, the disadvantage is that duplicate plaintexts will produce duplicate ciphertexts. + +Nonces are some extra cryptographic context that is used in many cryptographic algorithms to produce different ciphertexts, even if the plaintexts are the same. The nonce does not have to be secret, but it _does_ have to be unique. **pgsodium** comes with a useful function `pgsodium.crypto_aead_det_noncegen()` that will generate a cryptographically secure nonce for you, and in almost all cases it's best to use that function unless you know specifically what you are doing. In password hashing approaches, this is often similar to how a “salt” value is used to deduplicate password hashes. + +Duplicate ciphertexts cannot be used to “attack the key”, it can only reveal the duplication. However, duplication is still information. In our examples so far, an attacker might be able to use this information to determine that two accounts share the same secret. While not technically breaking the encryption, this still leaks information to an attacker. + +```sql +create table secrets ( + id bigserial primary key, + secret text, + key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid, + nonce bytea default pgsodium.crypto_aead_det_noncegen() +); + +security label for pgsodium + on column secrets.secret + is 'ENCRYPT WITH KEY COLUMN key_id NONCE nonce'; +``` + +This is the most secure form of TCE - there is a unique key ID and a unique nonce per row. + +### One Key ID per Row with Associated Data + +The encryption that is used for TCE is one of a family of functions provided by **libsodium** to do Authenticated Encryption with Associated Data or [AEAD Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption). The “associated” data is plaintext (unencrypted) information that is mixed into the authentication signature of the encrypted data, such that when you authenticate the data, you also know that the associated data is authentic. + +AEAD is helpful because often you have metadata associated with a secret, which isn't confidential but must not be forged. + +In our secret example, we might associate a "`secret`" with an "`account_id`". But what if a malicious actor wanted to use someone else's secret on their own account? If someone could forge the `account_id` data column, swapping an `account_id` with their own `account_id`, then you could be tricked into using the wrong secret. By “associating” the `account_id` with the `secret`, it cannot be forged without throwing an error. + +Like above, this is done simply by extending the security label with the associated data column: + +```sql +create table secrets ( + id bigserial primary key, + secret text, + account_id integer, + key_id uuid not null default 'e348034b-3f07-4878-aad6-000511d12826'::uuid, + nonce bytea default pgsodium.crypto_aead_det_noncegen() +); + +security label for pgsodium + on column secrets.secret + is 'ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (account_id) NONCE nonce'; +``` + +The new label indicates which column is to be associated with the secret, and that's it! Your `account_id` and secret are now protected under the same authentication signature as the secret itself. + +## Using an Encrypted Table + +Now that you have TCE setup for a table, it's easy to use by simply inserting data into the table, and querying that data by looking at its generated view. The view is named `decrypted_` and by default is in the same schema as your table: + +```sql +insert into secrets + (secret, account_id) +values + ('1234-5678-8765-4321', 123); +``` + +Now that you have inserted data, look at the table and notice how the secret is encrypted. This is the data that is stored on disk, the encrypted card number, the key id, and the account id, **but the key itself is not stored**. This means if someone gets a backup or dump of your database, they cannot decrypt the secret, they do not have the key, only the key ID: + +```sql +> select * from secrets where account_id = 123; +-[ RECORD 1 ]------+--------------------------------------------------------------------- +id | 1 +secret | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN +account_id | 123 +key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d +nonce | \x300a14aa721184ff7cf0f6bf088da267 +``` + +For you, the developer, you need the unencrypted secret for you application. No problem, you can access that data using the dynamically generated decryption view `decrypted_secrets`: + +```sql +> select * from decrypted_secrets where account_id = 123; +-[ RECORD 1 ]----------------+--------------------------------------------------------------------- +id | 1 +secret | jf8KfImkKTr+j4gzyDZQtLDEFL9eSlFuKjNlNEJvDg+OIKUr2wjF/8NnYcLisb5F9xiN +decrypted_secret | 1234-5678-8765-4321 +account_id | 123 +key_id | 7f753c4f-8c68-457a-8801-1798b2e9f44d +nonce | \x300a14aa721184ff7cf0f6bf088da267 +``` + +Notice how there is a new column called `decrypted_secret`. This column is not stored in database or on disk at all, it is generated “on-the-fly” as you select from the view. Database dumps do not contain this information, only the view itself, and most importantly, **raw decryption keys are never stored**. + +- [Supabase Vault](/docs/guides/database/vault) +- Read more about Supabase Vault in the [blog post](https://supabase.com/blog/vault-now-in-beta) +- [Supabase Vault on GitHub](https://github.com/supabase/vault) + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/database/vault.mdx b/apps/docs/pages/guides/database/vault.mdx new file mode 100644 index 00000000000..683d383219b --- /dev/null +++ b/apps/docs/pages/guides/database/vault.mdx @@ -0,0 +1,220 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'vault', + title: 'Vault', + description: 'Use Supabase to store and serve files.', + sidebar_label: 'Overview', + video: 'https://www.youtube.com/v/J9mTPY8rIXE', +} + +Supabase Vault provides encrypted secret storage and Encrypted Columns for Tables. + +Vault is a Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets and other data in your database. This opens up a lot of possibilities to use Postgres in ways that go beyond what is available in a stock distribution. + +From a product perspective, Supabase groups a number of related features under the “Vault banner”. Let's explore a few of these features. + +## Secrets Management + +Under the hood, the Vault is a table of Secrets and Encryption Keys that are stored using [Authenticated Encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) on disk. They are then available in decrypted form through a Postgres view so that the secrets can be used by applications from SQL. Because the secrets are stored on disk encrypted and authenticated, any backups or replication streams also preserve this encryption in a way that can't be decrypted or forged. + +Supabase provides a dashboard UI for the Vault that makes storing secrets easy. Click a button, type in your secret, and save. Optionally create your own keys you can use to encrypt your secret. Your secret will then be stored on disk encrypted using the specified key. + + + +There are two main parts to the Vault UI, Secrets and Encryption Keys: + +- **Secrets:** Use the Vault to store Secrets - everything from Environment Variables to API Keys. You can use these Secrets anywhere in your database: Postgres [Functions](/docs/guides/database/functions), Triggers, and [Webhooks](/docs/guides/database/webhooks). From a SQL perspective, accessing secrets is as easy as querying a table (or in this case, a view). The underlying secrets tables will be stored in encrypted form. +- **Encryption Keys:** These are keys used to encrypt data inside your database. You can create different Encryption Keys for different purposes, for example: one for encrypting user-data, and another for application-data. Each key is encrypted itself using a root encryption key that lives outside of the database. See **[Encryption key location](#encryption-key-location)** for more details. + +## Deep Dive on How The Vault works + +
+ +
+ +As we mentioned, the Vault uses pgsodium's Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details. + +### Authenticated Encryption with Associated Data + +The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data]() encryption algorithm (based on libsodium). + +### Encryption key location + +**Authenticated Encryption** means that in addition to the data being encrypted, it is also signed so that it cannot be forged. You can guarantee that the data was encrypted by someone you trust, which you wouldn't get with encryption alone. The decryption function verifies that the signature is valid _before decrypting the value_. + +**Associated Data** means that you can include any other columns from the same row as part of the signature computation. This doesn't encrypt those other columns - rather it ensures that your encrypted value is only associated with columns from that row. If an attacker were to copy an encrypted value from another row to the current one, the signature would be rejected (assuming you used a unique column in the associated data). + +Another important feature of pgsodium is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_. + +This is an important safety precaution - there is little value in storing the encryption key in the database itself as this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue. + +Where are the keys stored? Supabase creates and manages the root keys (from which all key IDs are derived) in our secured backend systems. We keep this root key safe and separate from your data. You remain in control of your keys - a separate API endpoint is available that you can use to access the key if you want to decrypt your data outside of Supabase. + +## Using the Vault + +Using the vault is as simple as `INSERT`ing data into the +`vault.secret` table. + +```sql +postgres=> insert into vault.secrets (secret) values ('s3kre3t_k3y') returning *; +-[ RECORD 1 ]------------------------------------------------------------- +id | d91596b8-1047-446c-b9c0-66d98af6d001 +name | +description | +secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== +key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 +nonce | \x3aa2e92f9808e496aa4163a59304b895 +created_at | 2022-12-14 02:29:21.3625+00 +updated_at | 2022-12-14 02:29:21.3625+00 +``` + +There is also a handy function for creating secrets called +`vault.create_secret()`: + +```sql +postgres=> select vault.create_secret('another_s3kre3t'); +-[ RECORD 1 ]-+------------------------------------- +create_secret | c9b00867-ca8b-44fc-a81d-d20b8169be17 + +``` + +The function returns the UUID of the new secret. + +## Name and Description + +Secrets can also have an optional _unique_ name, or an optional description. These are also arguments to `vault.create_secret()`: + +```sql +postgres=> select vault.create_secret('another_s3kre3t', 'unique_name', 'This is the description'); +-[ RECORD 1 ]-+------------------------------------- +create_secret | 7095d222-efe5-4cd5-b5c6-5755b451e223 + +postgres=> select * from vault.secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223'; +-[ RECORD 1 ]----------------------------------------------------------------- +id | 7095d222-efe5-4cd5-b5c6-5755b451e223 +name | unique_name +description | This is the description +secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= +key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +nonce | \x9f2d60954ba5eb566445736e0760b0e3 +created_at | 2022-12-14 02:34:23.85159+00 +updated_at | 2022-12-14 02:34:23.85159+00 +``` + +## Querying Data from the Vault + +If you look in the `vault.secrets` table, you will see that your data is stored encrypted. To decrypt the data, there is an automatically created view `vault.decrypted_secrets`. This view will decrypt secret data on the fly: + +```sql +postgres=> select * from vault.decrypted_secrets order by created_at desc limit 3; +-[ RECORD 1 ]----+----------------------------------------------------------------- +id | 7095d222-efe5-4cd5-b5c6-5755b451e223 +name | unique_name +description | This is the description +secret | 3mMeOcoG84a5F2uOfy2ugWYDp9sdxvCTmi6kTeT97bvA8rCEsG5DWWZtTU8VVeE= +decrypted_secret | another_s3kre3t +key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +nonce | \x9f2d60954ba5eb566445736e0760b0e3 +created_at | 2022-12-14 02:34:23.85159+00 +updated_at | 2022-12-14 02:34:23.85159+00 +-[ RECORD 2 ]----+----------------------------------------------------------------- +id | c9b00867-ca8b-44fc-a81d-d20b8169be17 +name | +description | +secret | a1CE4vXwQ53+N9bllJj1D7fasm59ykohjb7K90PPsRFUd9IbBdxIGZNoSQLIXl4= +decrypted_secret | another_s3kre3t +key_id | 8c72b05e-b931-4372-abf9-a09cfad18489 +nonce | \x1d3b2761548c4efb2d29ca11d44aa22f +created_at | 2022-12-14 02:32:50.58921+00 +updated_at | 2022-12-14 02:32:50.58921+00 +-[ RECORD 3 ]----+----------------------------------------------------------------- +id | d91596b8-1047-446c-b9c0-66d98af6d001 +name | +description | +secret | S02eXS9BBY+kE3r621IS8beAytEEtj+dDHjs9/0AoMy7HTbog+ylxcS22A== +decrypted_secret | s3kre3t_k3y +key_id | 7f5ad44b-6bd5-4c99-9f68-4b6c7486f927 +nonce | \x3aa2e92f9808e496aa4163a59304b895 +created_at | 2022-12-14 02:29:21.3625+00 +updated_at | 2022-12-14 02:29:21.3625+00 +``` + +Notice how this view has a `decrypted_secret` column that contains the decrypted secrets. Views are not stored on disk, they are only run at query time, so the secret remains encrypted on disk, and in any backup dumps or replication streams. + +You should ensure that you protect access to this view with the appropriate SQL privilege settings at all times, as anyone that has access to the view has access to decrypted secrets. + +## Updating Secrets + +A secret can be updated with the `vault.update_secret()` function, this function makes updating secrets easy, just provide the secret UUID as the first argument, and then an updated secret, updated optional unique name, or updated description: + +```sql +postgres=> select vault.update_secret('7095d222-efe5-4cd5-b5c6-5755b451e223', 'n3w_upd@ted_s3kret', + 'updated_unique_name', 'This is the updated description'); +-[ RECORD 1 ]-+- +update_secret | + +postgres=> select * from vault.decrypted_secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223'; +-[ RECORD 1 ]----+--------------------------------------------------------------------- +id | 7095d222-efe5-4cd5-b5c6-5755b451e223 +name | updated_unique_name +description | This is the updated description +secret | lhb3HBFxF+qJzp/HHCwhjl4QFb5dYDsIQEm35DaZQOovdkgp2iy6UMufTKJGH4ThMrU= +decrypted_secret | n3w_upd@ted_s3kret +key_id | c62da7a0-b85d-471d-8ea7-52aae21d7354 +nonce | \x9f2d60954ba5eb566445736e0760b0e3 +created_at | 2022-12-14 02:34:23.85159+00 +updated_at | 2022-12-14 02:51:13.938396+00 +``` + +## Internal Details + +To encrypt data, you need a _key id_. You can use the default key id created automatically for every project, or create your own key ids Using the `pgsodium.create_key()` function. Key ids are used to internally derive the encryption key used to encrypt secrets in the vault. Vault users typically do not have access to the key itself, only the key id. + +Both `vault.create_secret()` and `vault.update_secret()` take an optional fourth `new_key_id` argument. This argument can be used to store a different key id for the secret instead of the default value. + +```sql +postgres=> select vault.create_secret('another_s3kre3t_key', 'another_unique_name', + 'This is another description', (pgsodium.create_key()).id); +-[ RECORD 1 ]-+------------------------------------- +create_secret | cec9e005-a44d-4b19-86e1-febf3cd40619 +``` + +Which roles should have access to the `vault.secrets` table should be carefully considered. There are two ways to grant access, the first is that the `postgres` user can explicitly grant access to the vault table itself. + +## Turning off Statement Logging + +When you insert secrets into the vault table with an INSERT statement, those statements get logged by default into the Supabase logs. Since this would mean your secrets are stored unencrypted in the logs, you should turn off statement logging while using the Vault. + +While turning off statement logging does hinder you if you're used to looking at the logs to debug your application, it provides a much higher level of security by ensuring that your data does not leak out of the database and into the logs. This is especially critical with encrypted column data, because the statement logs will contain the _unencrypted_ secrets. If you _must_ store that data encrypted, then you _must_ turn off statement logging. + +```sql +alter system set statement_log = 'none'; +``` + +And then restart your project from the dashboard to enable that change. + +In the future we are researching various ways to refine the way statement logging interacts with sensitive columns. + +## See also + +- Read more about Supabase Vault in the [blog post](https://supabase.com/blog/vault-now-in-beta) +- [Supabase Vault on GitHub](https://github.com/supabase/vault) +- [Column Encryption](/docs/guides/database/column-encryption) + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/functions/examples/rate-limiting.mdx b/apps/docs/pages/guides/functions/examples/rate-limiting.mdx index 32486fadc48..6c99592a8fc 100644 --- a/apps/docs/pages/guides/functions/examples/rate-limiting.mdx +++ b/apps/docs/pages/guides/functions/examples/rate-limiting.mdx @@ -18,7 +18,7 @@ export const meta = { [Upstash](https://upstash.com/) provides an HTTP/REST based Redis client which is ideal for serverless use-cases and therefore works well with Supabase Edge Functions. -Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-counter). +Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-ratelimit). export const Page = ({ children }) => diff --git a/apps/docs/pages/guides/functions/quickstart.mdx b/apps/docs/pages/guides/functions/quickstart.mdx index 17f0ca95de5..be7537510fd 100644 --- a/apps/docs/pages/guides/functions/quickstart.mdx +++ b/apps/docs/pages/guides/functions/quickstart.mdx @@ -27,7 +27,7 @@ Follow the steps to prepare your Supabase project on your local machine. - Login to the CLI using the command: `supabase login`. [Docs](/docs/reference/cli/usage#supabase-login). - Initialize Supabase inside your project using the command: `supabase init`. [Docs](/docs/guides/getting-started/local-development#getting-started). - Link to your Remote Project using the command `supabase link --project-ref your-project-ref`. [Docs](/docs/reference/cli/usage#supabase-link). -- Optional: Setup your environment: Follow [this setup guide](https://deno.land/manual/getting_started/setup_your_environment) to integrate the Deno language server with your editor. +- Setup your environment: Follow [the steps below](/docs/guides/functions/quickstart#setting-up-your-environment). ## Create an Edge Function @@ -76,6 +76,7 @@ If you receive an error `Invalid JWT`, find the `ANON_KEY` of your project in th or using one of the [client libraries](/docs#reference-documentation), e.g. using [supabase-js](/docs/reference/javascript/functions-invoke): ```js +// https://supabase.com/docs/reference/javascript/installing import { createClient } from '@supabase/supabase-js' // Create a single supabase client for interacting with your database @@ -88,11 +89,51 @@ const { data, error } = await supabase.functions.invoke('hello-world', { After invoking your Edge Function you should see the response `{ "message":"Hello Functions!" }`. +## Importing Node npm modules + +We recommend using [esm.sh](https://esm.sh/) for importing Node.js modules that are published to npm. To do so you simply put `https://esm.sh/` in front of the package name. + +For example, when you want to use supabase-js within Supabase Edge Functions, you would import `createClient` as follows: + +```ts +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' +``` + +As long as your environment is set up properly and the module you're importing is exporting types, the import will have types and autocompletion support. + ## Setting Up Your Environment You can follow the [Deno guide](https://deno.com/manual@v1.32.5/getting_started/setup_your_environment) for setting up your development environment with your favorite editor/IDE. -When developing with VS Code inside of an existing application, you can utilize [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces). +### Deno with Visual Studio Code (vscode) + +Install the Deno language server [via this link](vscode:extension/denoland.vscode-deno) or by browsing the extensions in vscode and choosing to install the _Deno_ extension. + +#### Partially Deno enabling a workspace + +In a given workspace (or workspace folder), sub-paths can be enabled for Deno, while code outside those paths will be not be enabled and the vscode built-in JavaScript/TypeScript language server will be used. + +For example if you have a project like this: + +```txt +project +├── app +└── supabase + └── functions +``` + +Where you only want to enabled the `supabase/functions` path (and its subpaths) to be Deno enabled, you will want to add `./supabase/functions` to the list of _Deno: Enable Paths_ in the configuration. In your `.vscode/settings.json` file add: + +```json +{ + "deno.enablePaths": ["./supabase/functions"], + "deno.importMap": "./supabase/functions/import_map.json" +} +``` + +#### Multi-root workspaces + +Alternatively, you can utilize [multi-root workspaces](https://code.visualstudio.com/docs/editor/workspaces#_multiroot-workspaces).
+
+ You can find the [full example on GitHub](https://github.com/supabase-community/nextjs-openai-doc-search). @@ -458,7 +471,7 @@ All of this is glued together in a [Vercel Edge Function](https://vercel.com/doc In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx). -```tsx +```ts const handleConfirm = React.useCallback( async (query: string) => { setAnswer(undefined) diff --git a/apps/docs/pages/guides/integrations.mdx b/apps/docs/pages/guides/integrations.mdx index 1f491796c81..e23bc20609b 100644 --- a/apps/docs/pages/guides/integrations.mdx +++ b/apps/docs/pages/guides/integrations.mdx @@ -12,16 +12,16 @@ Explore a variety of integrations from Supabase partners. Need a different integ
{integrations.items.map((item) => ( -
+

{item.name}

{item.items?.map((integration) => ( - + } + icon={} > {integration.description} diff --git a/apps/docs/pages/guides/integrations/cloudflare-workers.mdx b/apps/docs/pages/guides/integrations/cloudflare-workers.mdx new file mode 100644 index 00000000000..352e8d9590f --- /dev/null +++ b/apps/docs/pages/guides/integrations/cloudflare-workers.mdx @@ -0,0 +1,108 @@ +import Layout from '~/layouts/DefaultGuideLayout' + +export const meta = { + id: 'cloudflare-workers', + title: 'Cloudflare Workers', + description: + "Using Supabase from your Cloudflare Workers just got even easier.", +} + +Using Supabase in Cloudflare Workers has always been a great way to interact with your data from the edge. Supabase-js communicates with your Supabase Postgres instance via HTTP using PostgREST, so you never need to worry about running out of database connections. + +In this guide we'll walk you through a new addition to the Cloudflare Workers dashboard - the ability to authenticate directly with your Supabase account, and automatically inject your Supabase environment variables into your Worker code. + +## How To Enable Supabase Integration in Cloudflare Workers + +Start by heading to the [Cloudflare Dashboard](https://dash.cloudflare.com), go to the Workers & Pages tab and hit 'Create Application' followed by 'Create Worker'. + +![Cloudflare Dashboard 2](/docs/img/guides/integrations/cloudflare-integration/2.png) + +Deploy the Hello World example Worker. Once it's deployed hit 'Configure Worker'. + +![Cloudflare Dashboard 3](/docs/img/guides/integrations/cloudflare-integration/3.png) + +On the configuration page select the Settings tab, followed by the Integrations option. + +You should now see the database integration options. On the Supabase card, click 'Add Integration' + +![Cloudflare Dashboard 4](/docs/img/guides/integrations/cloudflare-integration/4.png) + +After reviewing and accepting the terms, you will be shown the option to connect and a Supabase popup should appear. + +Follow the flow by selecting your Supabase Org and the Project you wish to connect to. If you don't have any projects yet, head over to the [Supabase Dashboard](https://app.supabase.com) to create one. + +![Cloudflare Dashboard 5](/docs/img/guides/integrations/cloudflare-integration/5.png) + +Once it's connected you will be given the option to select which Supabase Key you want to pull into the Worker context. + +The `Anon` key here is one that always adhires to the Database's RLS policies (read more on [Row Level Security](https://supabase.com/docs/guides/auth/row-level-security)). + +The `Service Role` is typically ok to use in backend contexts, such as Cloudflare Workers, but note that this key **bypasses your Row Level Security policies**, and has the ablity to read, write, and delete any data in your database. + +![Cloudflare Dashboard 6](/docs/img/guides/integrations/cloudflare-integration/6.png) + +Once this is done the `SUPABASE_KEY` and `SUPABASE_URL` environment variables will now be available from your Cloudflare Worker code. + +![Cloudflare Dashboard 7](/docs/img/guides/integrations/cloudflare-integration/7.png) + +You can now install the supabase-js client in your Worker: + +`npm install @supabase/supabase-js` + +Then you can initiate the Supabase client, and start querying your data: + +```javascript +import { createClient } from '@supabase/supabase-js' + +export default { + async fetch(request, env) { + const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY) + const { data, error } = await supabase.from('countries').select('*') + if (error) throw error + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + }, + }) + }, +} +``` + +The snippet above assumes you already have a `countries` table. Run the following in the [SQL Editor in the Supabase Dashboard](https://app.supabase.com/project/_/sql) if you wish to install this demo schema: + +```sql +create table countries ( + id serial primary key, + name varchar(255) not null +); + +insert into countries + (name) +values + ('Oceania'); + +insert into countries + (name) +values + ('Genovia'); + +insert into countries + (name) +values + ('Wakanda'); + +insert into countries + (name) +values + ('Lilliput'); +``` + +Remember that you don't need to use supabase-js to connect to your Supabase database, you can connect "directly" to the underlying Postgres database using the connection string (every Supabase database comes pre-installed with a [connection pooler](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool)), or you can try Cloudflare's new [TCP socket method of connecting to Postgres](https://blog.cloudflare.com/workers-tcp-socket-api-connect-databases/) directly from Cloudflare Workers. + +- [Cloudflare Integration Docs](https://developers.cloudflare.com/workers/learning/integrations/databases/#supabase). +- [Cloudflare Dashboard](dash.cloudflare.com/). +- [Cloudflare Integration Announcement](https://blog.cloudflare.com/announcing-database-integrations/). + +export const Page = ({ children }) => + +export default Page diff --git a/apps/docs/pages/guides/platform/logs.mdx b/apps/docs/pages/guides/platform/logs.mdx index 95880b26fc2..5ca95f3624a 100644 --- a/apps/docs/pages/guides/platform/logs.mdx +++ b/apps/docs/pages/guides/platform/logs.mdx @@ -112,7 +112,28 @@ To _permanently_ set a logging configuration (beyond a single session), execute alter role postgres set pgaudit.log to 'function, write, ddl'; ``` -For user-traffic flowing through the [HTTP APIs](../../guides/database/api#rest-api-overview) powered by PostgREST, set your configuration values for the roles `anon` and `authenticated`. For traffic using the `service_role` key, set the configuration values for the role `service_role`. +To help with debugging, we recommend adjusting the log scope to only relevant statements as having too wide of a scope would result in a lot of noise in your Postgres logs. + +Note that in the above example, the role is set to `postgres`. To log user-traffic flowing through the [HTTP APIs](../../guides/database/api#rest-api-overview) powered by PostgREST, set your configuration values for the roles `anon` and `authenticated`. For traffic using the `service_role` key, set the configuration values for the role `service_role`. + +```sql +-- for API-related logs +alter role anon set pgaudit.log to 'write'; +alter role authenticated set pgaudit.log to 'write'; + +-- for service role logs +alter role service_role set pgaudit.log to 'write'; +``` + +By default, the log level will be set to `log`. To view other levels, run the following: + +```sql +-- adjust log level +alter role postgres set pgaudit.log_level to 'info'; +alter role postgres set pgaudit.log_level to 'debug5'; +``` + +Note that as per the pgAudit [log_level documentation](https://github.com/pgaudit/pgaudit/blob/master/README.md#pgauditlog_level), `error`, `fatal`, and `panic` are not allowed. To reset system-wide settings, execute the following, then perform a fast reboot: diff --git a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx index 0bb0f3b310a..cebf408bb77 100644 --- a/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx +++ b/apps/docs/pages/guides/platform/migrating-and-upgrading-projects.mdx @@ -49,7 +49,6 @@ In your new project: 1. Enable [Database Webhooks](https://app.supabase.com/project/_/database/hooks) if you enabled them in your old project. 2. Enable any [extensions](https://app.supabase.com/project/_/database/extensions) that were enabled in your old project. -3. Create any [custom roles](https://app.supabase.com/project/_/database/roles) that were created in your old project. Then run the following command from your terminal: @@ -57,11 +56,14 @@ Then run the following command from your terminal: psql \ --single-transaction \ --variable ON_ERROR_STOP=1 \ + --file roles.sql \ --file schema.sql \ --file data.sql \ --dbname "$NEW_DB_URL" ``` +Note if you have created any [custom roles](https://app.supabase.com/project/_/database/roles) with `login` attribute, you have to manually set their passwords in the new project. + ### Enable publication on tables Replication for Realtime is disabled for all tables in your new project. On the [Replication](https://app.supabase.com/project/_/database/replication) page in the Dashboard, select your new project and enable replication for tables that were enabled in your old project. diff --git a/apps/docs/pages/guides/platform/shared-responsibility-model.mdx b/apps/docs/pages/guides/platform/shared-responsibility-model.mdx index 93370358283..9e624b7ec9c 100644 --- a/apps/docs/pages/guides/platform/shared-responsibility-model.mdx +++ b/apps/docs/pages/guides/platform/shared-responsibility-model.mdx @@ -18,12 +18,6 @@ If you have an inexperienced member on your team, then you probably shouldn’t You are also responsible for ensuring that tables with sensitive data have the right level of access. You are also responsible for managing your database secrets and API keys, storing them safely in an encrypted store. -## You can crash your database - -”Crash” is used liberally here. It refers to any situation where your application is unable to work as desired because of DB issues. It could be because the DB is unable to support the number of connections being attempted, or because its performance is unacceptably low because of CPU or memory constraints. - -You are responsible of provisioning enough compute to run the workload that your application requires. The Supabase Dashboard provides [observability tooling](https://app.supabase.com/project/_/reports/database) to help with this. You are also responsible for employing methods to manage the workload: adding indexes, adding filters on large queries, caching strategies, materialized views, and optimizing your database queries. - ## You decide your own workflow There are _many_ ways to work with Supabase. @@ -44,6 +38,14 @@ If you don’t want to implement logic inside your database, that is 100% fine. Our goal at Supabase is to make _all_ of Postgres easy to use. That doesn’t mean you have to use all of it. If you’re a Postgres veteran, you’ll probably love the tools that we offer. If you’ve never used Postgres before, then start smaller and grow into it. If you just want to treat Postgres like a simple table-store, that’s perfectly fine. +## You are in control of your database + +Supabase places very few guard-rails around your database. That gives you a lot of control, but it also means you can break things. ”Break” is used liberally here. It refers to any situation that affects your application because of the way you're using the database. + +You are responsible for using best-practices to optimize and manage your database: adding indexes, adding filters on large queries, using caching strategies, optimizing your database queries, and managing connections to the database. + +You are responsible of provisioning enough compute to run the workload that your application requires. The Supabase Dashboard provides [observability tooling](https://app.supabase.com/project/_/reports/database) to help with this. + export const Page = ({ children }) => export default Page diff --git a/apps/docs/pages/guides/self-hosting.mdx b/apps/docs/pages/guides/self-hosting.mdx index 39a57fb549e..fa4d6f0291a 100644 --- a/apps/docs/pages/guides/self-hosting.mdx +++ b/apps/docs/pages/guides/self-hosting.mdx @@ -134,7 +134,7 @@ secrets manager when deploying to production. Plain text files like dotenv lead Some suggested systems include: - [Doppler](https://www.doppler.com/) -- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure +- [Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/overview) by Azure (Microsoft) - [Secrets Manager](https://aws.amazon.com/secrets-manager/) by AWS - [Secrets Manager](https://cloud.google.com/secret-manager) by GCP - [Vault](https://www.hashicorp.com/products/vault) by Hashicorp diff --git a/apps/docs/public/img/guides/database/og-vault.png b/apps/docs/public/img/guides/database/og-vault.png new file mode 100644 index 00000000000..71ff756fef8 Binary files /dev/null and b/apps/docs/public/img/guides/database/og-vault.png differ diff --git a/apps/docs/public/img/guides/database/vault-decrypted-data.png b/apps/docs/public/img/guides/database/vault-decrypted-data.png new file mode 100644 index 00000000000..e802f9d1ef3 Binary files /dev/null and b/apps/docs/public/img/guides/database/vault-decrypted-data.png differ diff --git a/apps/docs/public/img/guides/database/vault-encrypted-data.png b/apps/docs/public/img/guides/database/vault-encrypted-data.png new file mode 100644 index 00000000000..631536c56fa Binary files /dev/null and b/apps/docs/public/img/guides/database/vault-encrypted-data.png differ diff --git a/apps/docs/public/img/guides/database/vault-encrypting-columns.png b/apps/docs/public/img/guides/database/vault-encrypting-columns.png new file mode 100644 index 00000000000..2a2c487eaa7 Binary files /dev/null and b/apps/docs/public/img/guides/database/vault-encrypting-columns.png differ diff --git a/apps/docs/public/img/guides/database/vault-hello-compressed.mp4 b/apps/docs/public/img/guides/database/vault-hello-compressed.mp4 new file mode 100644 index 00000000000..48395586435 Binary files /dev/null and b/apps/docs/public/img/guides/database/vault-hello-compressed.mp4 differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/2.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/2.png new file mode 100644 index 00000000000..4348e508742 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/2.png differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/3.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/3.png new file mode 100644 index 00000000000..2a6db2dc104 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/3.png differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/4.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/4.png new file mode 100644 index 00000000000..d1aba0bb706 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/4.png differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/5.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/5.png new file mode 100644 index 00000000000..1db9444f210 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/5.png differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/6.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/6.png new file mode 100644 index 00000000000..0c7ce386ac6 Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/6.png differ diff --git a/apps/docs/public/img/guides/integrations/cloudflare-integration/7.png b/apps/docs/public/img/guides/integrations/cloudflare-integration/7.png new file mode 100644 index 00000000000..f6d4c49df5f Binary files /dev/null and b/apps/docs/public/img/guides/integrations/cloudflare-integration/7.png differ diff --git a/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.jpeg b/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.jpeg new file mode 100644 index 00000000000..6d09d84acc5 Binary files /dev/null and b/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.jpeg differ diff --git a/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.png b/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.png new file mode 100644 index 00000000000..af9e1b1a658 Binary files /dev/null and b/apps/docs/public/img/integrations/logos/cloudflare_workers_logo.png differ diff --git a/apps/docs/public/img/integrations/logos/upstash_redis_logo.jpeg b/apps/docs/public/img/integrations/logos/upstash_redis_logo.jpeg new file mode 100644 index 00000000000..846d395ccf3 Binary files /dev/null and b/apps/docs/public/img/integrations/logos/upstash_redis_logo.jpeg differ diff --git a/apps/docs/public/img/integrations/logos/upstash_redis_logo.png b/apps/docs/public/img/integrations/logos/upstash_redis_logo.png new file mode 100644 index 00000000000..eb0df80afd9 Binary files /dev/null and b/apps/docs/public/img/integrations/logos/upstash_redis_logo.png differ diff --git a/apps/docs/public/sitemap.xml b/apps/docs/public/sitemap.xml index 78ce4d811b9..c3d36c42b96 100644 --- a/apps/docs/public/sitemap.xml +++ b/apps/docs/public/sitemap.xml @@ -552,6 +552,12 @@ 0.5 + + https://supabase.com/docs/guides/integrations/cloudflare-workers + weekly + 0.5 + + https://supabase.com/docs/guides/integrations/dhiwise weekly 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 367cc275bc9..2ac7e4607f6 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 @@ -12,16 +12,6 @@ date: '2022-06-30' toc_depth: 3 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- This is the first of a series of Flutter Tutorials. During the series, you will learn how to build cross-platform apps without worrying about the backend. 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. 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 86fb04f8a07..cdabad21e70 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 @@ -11,16 +11,6 @@ date: '2022-08-02' toc_depth: 3 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- Today, we are releasing of Developer Preview version of v1.0 of [Supabase Flutter SDK](https://pub.dev/packages/supabase_flutter/versions/1.0.0-dev.1). Flutter has quickly become one of the most popular frameworks for developers to build cross-platform mobile apps. We can attest to that growth, our Flutter SDK is one of the most popular libraries and each day we see more Flutter devs choosing Supabase. For this release, our main focus is developer experiences. We would love for you to try the SDK and provide your feedback so that we can continue to improve! diff --git a/apps/www/_blog/2022-08-17-supabase-soc2.mdx b/apps/www/_blog/2022-08-17-supabase-soc2.mdx index c3f4fa8ff6a..23c53ac904d 100644 --- a/apps/www/_blog/2022-08-17-supabase-soc2.mdx +++ b/apps/www/_blog/2022-08-17-supabase-soc2.mdx @@ -11,6 +11,10 @@ toc_depth: 3 video: https://www.youtube.com/v/6bGQotxisoY --- +Update (2023-05-22): Supabase is now SOC2 Type 2 compliant. Read more about security at Supabase in our [Security Center](https://supabase.com/security). + +--- + Supabase is now SOC2 Type 1 compliant. Let’s dig into what that means, and explain the process we went through to get there. If you’re building a SaaS product, this blog post should provide a rough guide to getting your SOC2 certification. ## What is SOC2? @@ -88,7 +92,7 @@ Today we are launching our [security center](https://supabase.com/security), whi ## What’s next -Getting the Type 1 certification is just the start, and we will be working on getting certified for SOC2 Type 2 and HIPAA next. If you are interested in discussing these, we would love to chat. We will also be rolling out a series of security improvements to all projects hosted on the Supabase Cloud offering over the coming months. Stay tuned! +Getting the SOC2 certification is just the start, and we will be working on getting certified for HIPAA next. If you are interested in discussing these, we would love to chat. We will also be rolling out a series of security improvements to all projects hosted on the Supabase Cloud offering over the coming months. Stay tuned! ## Announcement video and discussion 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 da6bbce5f32..8709747976a 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 @@ -11,16 +11,6 @@ date: '2022-10-21' toc_depth: 3 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- A few months ago, we announced a [developer preview version of supabase-flutter SDK](https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview). Since then, we have heard a lot of amazing feedback from the community, and have been improving it. Today, we are happy to announce the stable v1 of [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can also find the updated [quick start guide](https://supabase.com/docs/guides/with-flutter), [documentation](https://supabase.com/docs/reference/dart) and a [migration guide from v0](https://supabase.com/docs/reference/dart/v0/upgrade-guide). ## What is new in v1? 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 index 6fca2dc92e6..93ed654d530 100644 --- 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 @@ -12,16 +12,6 @@ date: '2022-11-22' toc_depth: 3 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- 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). diff --git a/apps/www/_blog/2023-02-14-flutter-real-time-multiplayer-game.mdx b/apps/www/_blog/2023-02-14-flutter-real-time-multiplayer-game.mdx index f6528a1740f..4c5952c6ca9 100644 --- a/apps/www/_blog/2023-02-14-flutter-real-time-multiplayer-game.mdx +++ b/apps/www/_blog/2023-02-14-flutter-real-time-multiplayer-game.mdx @@ -12,16 +12,6 @@ date: '2023-02-14' toc_depth: 3 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- Flutter is a UI library to build apps that run on any platform, but it can also build interactive games thanks to an open-source game engine built on top of Flutter called [Flame](https://flame-engine.org/). Flame takes care of things like collision detection or loading image sprites to bring game development to all the Flutter devs. We can take it a step further to introduce real-time communication features so that players can play against each other in real-time. In this article, we will use Flutter, Flame, and Supabase's real-time features to build a real-time multiplayer shooting game. You can find the complete code of this tutorial [here](https://github.com/supabase/supabase/tree/master/examples/realtime/flutter-multiplayer-shooting-game). diff --git a/apps/www/_blog/2023-04-10-supabase-logs-self-hosted.mdx b/apps/www/_blog/2023-04-10-supabase-logs-self-hosted.mdx index dc65f930ce1..e036b8be70a 100644 --- a/apps/www/_blog/2023-04-10-supabase-logs-self-hosted.mdx +++ b/apps/www/_blog/2023-04-10-supabase-logs-self-hosted.mdx @@ -1,5 +1,6 @@ --- title: 'Supabase Logs: open source logging server' +description: We're releasing Supabase Logs for both self-hosted users and CLI development. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-11-edge-runtime-self-hosted-deno-functions.mdx b/apps/www/_blog/2023-04-11-edge-runtime-self-hosted-deno-functions.mdx index b6f725695f8..5451ddb58b1 100644 --- a/apps/www/_blog/2023-04-11-edge-runtime-self-hosted-deno-functions.mdx +++ b/apps/www/_blog/2023-04-11-edge-runtime-self-hosted-deno-functions.mdx @@ -1,5 +1,6 @@ --- title: 'Supabase Edge Runtime: Self-hosted Deno Functions' +description: We are open-sourcing Supabase Edge Runtime allowing you to host your Edge Functions anywhere. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-12-storage-v3-resumable-uploads.mdx b/apps/www/_blog/2023-04-12-storage-v3-resumable-uploads.mdx index 013343df4c5..57db2288f05 100644 --- a/apps/www/_blog/2023-04-12-storage-v3-resumable-uploads.mdx +++ b/apps/www/_blog/2023-04-12-storage-v3-resumable-uploads.mdx @@ -1,5 +1,6 @@ --- title: 'Supabase Storage v3: Resumable Uploads with support for 50GB files' +description: Storage V3 with lots of new features including resumable uploads, more image transformationsm a Next.js image loader and more. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-13-supabase-auth-sso-pkce.mdx b/apps/www/_blog/2023-04-13-supabase-auth-sso-pkce.mdx index b0ff0777bf7..c886c97a692 100644 --- a/apps/www/_blog/2023-04-13-supabase-auth-sso-pkce.mdx +++ b/apps/www/_blog/2023-04-13-supabase-auth-sso-pkce.mdx @@ -1,5 +1,6 @@ --- title: 'Supabase Auth: SSO, Mobile, and Server-side support' +description: Supacharging Supabase Auth with Sign in with Apple on iOS, Single-Sign-On support with SAML 2.0, and PKCE for server-side rendering and mobile auth. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-14-launch-week-7-community-highlights.mdx b/apps/www/_blog/2023-04-14-launch-week-7-community-highlights.mdx index a87c43050d8..5f74875cf0d 100644 --- a/apps/www/_blog/2023-04-14-launch-week-7-community-highlights.mdx +++ b/apps/www/_blog/2023-04-14-launch-week-7-community-highlights.mdx @@ -1,5 +1,6 @@ --- title: 'Launch Week 7 Community Highlights' +description: We're honored to work with, sponsor, and support incredible people and tools. Here is a highlight of the last 3 months. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-14-pg-tle.mdx b/apps/www/_blog/2023-04-14-pg-tle.mdx index 52aef363a48..cf565c6d3cb 100644 --- a/apps/www/_blog/2023-04-14-pg-tle.mdx +++ b/apps/www/_blog/2023-04-14-pg-tle.mdx @@ -1,5 +1,6 @@ --- title: 'Trusted Language Extensions for Postgres' +description: We're collaborating with AWS to bring Trusted Language Extensions to Postgres. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-04-14-supabase-studio-2.0.mdx b/apps/www/_blog/2023-04-14-supabase-studio-2.0.mdx index 4b7464c4a77..a31447ec46d 100644 --- a/apps/www/_blog/2023-04-14-supabase-studio-2.0.mdx +++ b/apps/www/_blog/2023-04-14-supabase-studio-2.0.mdx @@ -1,5 +1,6 @@ --- title: 'Supabase Studio 2.0: help when you need it most' +description: Supabase Studio now comes with ChatGPT, and GraphiQL built in, Cascade Deletes, and Foreign Key Selectors, and much more. launchweek: 7 tags: - launch-week diff --git a/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx b/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx index 57afdbbccee..ad11ad53020 100644 --- a/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx +++ b/apps/www/_blog/2023-05-04-flutter-multi-factor-authentication.mdx @@ -10,16 +10,6 @@ date: '2023-05-04' toc_depth: 2 --- -
- -🚨 We are currently running our first-ever Flutter Hackathon, in partnership with FlutterFlow and Invertase 🚨 - -Build and submit an app till 11:59 pm PT on Sunday, May 21st and win limited edition swag! - -[Learn more.](https://supabase.com/blog/flutter-hackathon) - -
- Multi-factor authentication or MFA is an essential part of security for any kind of app. We will take a look at an example app where a user has to sign in via MFA in order to view the contents of the app to demonstrate how easy it is to get started with MFA on Flutter. diff --git a/apps/www/_blog/2023-05-15-building-chatgpt-plugins-template.mdx b/apps/www/_blog/2023-05-15-building-chatgpt-plugins-template.mdx index 56d508212f8..3e123d34fae 100644 --- a/apps/www/_blog/2023-05-15-building-chatgpt-plugins-template.mdx +++ b/apps/www/_blog/2023-05-15-building-chatgpt-plugins-template.mdx @@ -1,5 +1,6 @@ --- title: 'Building ChatGPT Plugins with Supabase Edge Runtime' +description: We're releasing a ChatGPT plugin template written in TypeScript and running on Deno! tags: - AI - tutorial diff --git a/apps/www/_customers/epsilon3.mdx b/apps/www/_customers/epsilon3.mdx index 6c6c94f7ac4..1cfba9ea83d 100644 --- a/apps/www/_customers/epsilon3.mdx +++ b/apps/www/_customers/epsilon3.mdx @@ -1,7 +1,11 @@ --- -title: Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations. name: Epsilon3 +title: Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions. author: rory_wilding author_title: Supabase author_url: https://github.com/kiwicopple diff --git a/apps/www/_customers/happyteams.mdx b/apps/www/_customers/happyteams.mdx index 9a36c5ba610..78f713bd753 100644 --- a/apps/www/_customers/happyteams.mdx +++ b/apps/www/_customers/happyteams.mdx @@ -1,7 +1,11 @@ --- -title: HappyTeams unlocks better performance and reduces cost with Supabase. name: HappyTeams +title: HappyTeams unlocks better performance and reduces cost with Supabase. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back. author: rory_wilding author_title: Supabase author_url: https://github.com/kiwicopple diff --git a/apps/www/_customers/markprompt.mdx b/apps/www/_customers/markprompt.mdx new file mode 100644 index 00000000000..200ae34564d --- /dev/null +++ b/apps/www/_customers/markprompt.mdx @@ -0,0 +1,81 @@ +--- +title: Markprompt and Supabase - GDPR-Compliant AI Chatbots for Docs and Websites. +name: Markprompt +description: AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions. +author: paul_copplestone +author_title: Supabase +author_url: https://github.com/kiwicopple +author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4 +authorURL: https://github.com/kiwicopple +logo: /images/customers/logos/markprompt.png +logo_inverse: /images/customers/logos/light/markprompt.png +og_image: /images/customers/og/markprompt.png +tags: + - supabase +date: '2023-05-17' +company_url: 'https://markprompt.com/' +stats: + [ + { stat: '00,000', label: Example stat }, + { stat: '00,000', label: Example stat }, + { stat: '00,000', label: Example stat }, + ] +misc: [{ label: 'Built by', text: 'The Motif Team' }] +about: Enterprise-grade AI chatbots for your website and docs. +--- + +Markprompt is an AI-powered chatbot platform that simplifies the process of finding relevant information in large-scale knowledge bases. With its chat-based interface, users can ask questions in natural language and receive instant, accurate answers. Markprompt's advanced analytics help teams identify knowledge gaps and improve their documentation strategies. + + + We’ve found that having access to the full features of Postgres, colocated with the embeddings, + makes it the perfect vector database. + + +## The Challenge + +When developing the platform, Markprompt considered GDPR compliance crucial as it enabled them to handle user data responsibly, maintain data privacy, and foster trust and confidence among their customers. As a result, the choice of a robust database and authentication solution became vital in ensuring adherence to GDPR requirements. + +Markprompt has successfully indexed over half a million sections of content, with a steady influx of 10,000 - 50,000 new sections daily. This emphasizes the significance of their vector database solution in terms of reliability and scalability. + + + We decided to use Supabase over other specialized vector databases because it enabled us to be + GDPR compliant from day one with little effort. + + +## Why they chose Supabase: + +By utilizing a trusted solution like Supabase for their database and authentication needs, Markprompt can implement access controls, user consent management, and audit trails, which are essential elements for GDPR compliance. This ensures that personal data is handled with care, protected from unauthorized access, and enables Markprompt to meet the strict data protection standards set forth by the GDPR. + +Markprompt were also impressed with the ease of use and scalability of Supabase. The platform allowed them to focus on building their product, knowing that they had a powerful and reliable database solution backing them up. + + + Supabase has been a joy to work with. Handling enterprise data at scale can be daunting, but we + feel we are in good hands as we grow. + + +## What They Built + +Markprompt is a set of API endpoints that allow you to train your content and create a prompt to ask questions to it, for instance for a docs site. Markprompt also designed a user-friendly web dashboard that simplifies content training, synchronization with GitHub repos or websites, access key management, and provides insights into user query statistics. Furthermore, Markprompt's UI components, available as React and Web Components, allow integration of prompts at all touch points of a company, from public websites websites to internal knowledge bases with just a few lines of code. + +Like Supabase, Markprompt is an open-source platform, allowing customers the freedom to host the dashboard and model backend on their own premises. Markprompt warmly welcomes contributions from the community, fostering collaboration and continuous improvement of the platform to meet the evolving needs of developers and technical users. + + + Building everything on Supabase from the Auth, to DB, to the vectors—this integrated experience + really made it for us. + + +## The Results + +Choosing Supabase as their database and authentication solution empowered Markprompt to focus on developing innovative AI-powered solutions without compromising data security or impeding scalability. The seamless integration between Supabase and pgvector enabled them to meet the increasing demands of their expanding user base while upholding stringent GDPR requirements. Consequently, companies gain trust and confidence in the platform, solidifying its reputation as a robust enterprise offering. + +## Tech stack + +- Next.js - framework +- Vercel - hosting +- Typescript - language +- Tailwind - CSS +- Upstash - Redis and rate limiting +- Supabase - database and auth +- Stripe - payments +- Plain - support chat +- Fathom - analytics diff --git a/apps/www/_customers/mendableai.mdx b/apps/www/_customers/mendableai.mdx index 1a911c9f952..8b59a0c7246 100644 --- a/apps/www/_customers/mendableai.mdx +++ b/apps/www/_customers/mendableai.mdx @@ -1,7 +1,11 @@ --- -title: Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings. name: Mendable +title: Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector. author: paul_copplestone author_title: Supabase author_url: https://github.com/kiwicopple diff --git a/apps/www/_customers/mobbin.mdx b/apps/www/_customers/mobbin.mdx index 06774869a3e..3149e98f8dd 100644 --- a/apps/www/_customers/mobbin.mdx +++ b/apps/www/_customers/mobbin.mdx @@ -1,7 +1,11 @@ --- -title: How Mobbin migrated 200,000 users from Firebase for a better authentication experience. name: Mobbin +title: How Mobbin migrated 200,000 users from Firebase for a better authentication experience. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps. author: rory_wilding author_title: Supabase author_url: https://github.com/kiwicopple diff --git a/apps/www/_customers/replenysh.mdx b/apps/www/_customers/replenysh.mdx index d6454cde443..32e05a4d2a3 100644 --- a/apps/www/_customers/replenysh.mdx +++ b/apps/www/_customers/replenysh.mdx @@ -1,7 +1,11 @@ --- -title: Replenysh uses Supabase to implement OTP in less than 24 hours. name: Replenysh +title: Replenysh uses Supabase to implement OTP in less than 24 hours. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres. author: rory_wilding author_title: Supabase author_url: https://github.com/kiwicopple diff --git a/apps/www/_customers/xendit.mdx b/apps/www/_customers/xendit.mdx index 7f3b8a131f7..6f05ecafc60 100644 --- a/apps/www/_customers/xendit.mdx +++ b/apps/www/_customers/xendit.mdx @@ -1,7 +1,11 @@ --- -title: Xendit use Supabase and create a full solution shipped to production in less than one week. name: Xendit +title: Xendit use Supabase and create a full solution shipped to production in less than one week. +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: description: As a payment processor, Xendit are responsible for verifying that all transactions are legal. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: As a payment processor, Xendit are responsible for verifying that all transactions are legal. author: rory_wilding author_title: Supabase author_url: https://github.com/kiwicopple @@ -10,10 +14,10 @@ authorURL: https://github.com/kiwicopple logo: /images/customers/logos/xendit.png logo_inverse: /images/customers/logos/light/xendit.png og_image: /images/customers/og/xendit.jpg -company_url: https://xendit.co tags: - supabase date: '2023-02-14' +company_url: https://xendit.co stats: [ { stat: '120', label: Staff count }, diff --git a/apps/www/components/BackedBy.tsx b/apps/www/components/BackedBy.tsx new file mode 100644 index 00000000000..f796653e736 --- /dev/null +++ b/apps/www/components/BackedBy.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { useRouter } from 'next/router' + +const BackedBy = ({ + className, + layout = 'vertical', +}: { + className?: string + layout?: 'horizontal' | 'vertical' +}) => { + const { basePath } = useRouter() + + return ( +
+ backed by +
+
+ Y Combinator + Mozilla + Coatue + Felicis +
+
+
+ ) +} + +export default BackedBy diff --git a/apps/www/components/BuiltWithSupabase/index.tsx b/apps/www/components/BuiltWithSupabase/index.tsx index b467d87e570..de349708517 100644 --- a/apps/www/components/BuiltWithSupabase/index.tsx +++ b/apps/www/components/BuiltWithSupabase/index.tsx @@ -17,7 +17,7 @@ const BuiltExamples = () => {
- diff --git a/apps/www/components/Carousels/ImageCarousel.tsx b/apps/www/components/Carousels/ImageCarousel.tsx index a9b45a5597e..ef544266da7 100644 --- a/apps/www/components/Carousels/ImageCarousel.tsx +++ b/apps/www/components/Carousels/ImageCarousel.tsx @@ -125,6 +125,7 @@ function ImageCarousel(props: ImageCarouselProps) { style={{ top: 0, left: 0 }} frameBorder="0" allow="autoplay; modestbranding; encrypted-media" + loading="lazy" />
)} diff --git a/apps/www/components/CodeBlock/CodeBlock.tsx b/apps/www/components/CodeBlock/CodeBlock.tsx index 7b40664cea9..732f558354e 100644 --- a/apps/www/components/CodeBlock/CodeBlock.tsx +++ b/apps/www/components/CodeBlock/CodeBlock.tsx @@ -126,6 +126,7 @@ function CodeBlock(props: CodeBlockProps) { ) } onClick={() => handleCopy()} + aria-label="Copy" > {/* {copied ? 'Copied' : 'Copy'} */} diff --git a/apps/www/components/ExampleCard.tsx b/apps/www/components/ExampleCard.tsx index 247eb9b8fa8..d0265ab6b8c 100644 --- a/apps/www/components/ExampleCard.tsx +++ b/apps/www/components/ExampleCard.tsx @@ -23,7 +23,7 @@ function ExampleCard(props: any) { {props.author {props.author}
@@ -53,10 +53,14 @@ function ExampleCard(props: any) { -
+
{props.vercel_deploy_url && ( - vercel button + vercel button )} {props.demo_url && ( diff --git a/apps/www/components/Features/index.tsx b/apps/www/components/Features/index.tsx index ab6510718e5..4af3829d6fe 100644 --- a/apps/www/components/Features/index.tsx +++ b/apps/www/components/Features/index.tsx @@ -49,7 +49,7 @@ const Features = () => { const IconSections = Object.values(Solutions).map((solution: any) => { const { name, description, icon, label, url } = solution - if (solution.name === 'Realtime') return null + return (
@@ -77,8 +77,8 @@ const Features = () => { return ( -

Build faster and focus on your products

-
+

Build faster and focus on your products

+
{IconSections}
diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx deleted file mode 100644 index d2decb0ab08..00000000000 --- a/apps/www/components/Hero.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { Button, IconBookOpen } from 'ui' -import { useRouter } from 'next/router' -import Link from 'next/link' -import SectionContainer from './Layouts/SectionContainer' -import Telemetry, { TelemetryEvent } from '~/lib/telemetry' -import gaEvents from '~/lib/gaEvents' -import { useTelemetryProps } from 'common/hooks/useTelemetryProps' - -const Hero = () => { - const router = useRouter() - const { basePath } = router - const telemetryProps = useTelemetryProps() - - const sendTelemetryEvent = async (event: TelemetryEvent) => { - await Telemetry.sendEvent(event, telemetryProps, router) - } - - return ( -
- -
-
-
-
-
-
-
-

- Build in a weekend. - Scale to millions. -

-
-

- Supabase is an open source Firebase alternative. Start your project with a - Postgres database, Authentication, instant APIs, Edge Functions, Realtime - subscriptions, and Storage. -

-
-
- -
- backed by -
-
- Y Combinator - Mozilla - Coatue - Felicis -
-
-
-
-
-
-
-
-
-
-
-
-
-