Merge pull request #14456 from supabase/master

Prod deploy dashboard
This commit is contained in:
Alaister Young authored and GitHub committed 2023-05-23 15:54:47 +10:00
commit fa3eacc363
140 files changed
+2017 -543

No files matched your search

+30
View File
@@ -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
@@ -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 ')
})
})
})
})
@@ -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
@@ -9,8 +9,8 @@ export interface NavMenuGroup {
export interface NavMenuSection {
name: string
url?: string
items: NavMenuSection[]
url?: `/${string}`
items: Partial<NavMenuSection>[]
}
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<Partial<NavMenuSection>>
}>
@@ -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' },
@@ -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)
}
})
+1 -1
View File
@@ -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,
+9
View File
@@ -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
+1
View File
@@ -62,6 +62,7 @@ const nextConfig = {
'raw.githubusercontent.com',
'weweb-changelog.ghost.io',
'img.youtube.com',
'archbee-image-uploads.s3.amazonaws.com'
],
},
experimental: {
+1
View File
@@ -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",
@@ -249,6 +249,8 @@ export const useSupabase = () => {
Modify `layout.jsx` to wrap the application with the `<SupabaseProvider>` 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 (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
@@ -278,6 +286,8 @@ export default function RootLayout({ children }) {
Modify `layout.tsx` to wrap the application with the `<SupabaseProvider>` 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 (
<html lang="en">
<body>
<SupabaseProvider session={session}>
<SupabaseListener serverAccessToken={session?.access_token} />
{children}
</SupabaseProvider>
<SupabaseProvider session={session}>{children}</SupabaseProvider>
</body>
</html>
)
@@ -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
@@ -306,3 +306,7 @@ export const Page = ({ children }) => <Layout meta={meta} children={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.
@@ -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
@@ -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_<your-table-name>`. 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_<table_name>` 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 }) => <Layout meta={meta} children={children} />
export default Page
+220
View File
@@ -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.
<video width="99%" muted playsInline controls="true">
<source
src="/docs/img/guides/database/vault-hello-compressed.mp4"
type="video/mp4"
muted
playsInline
/>
</video>
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
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/QHLPNDrdN2w"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
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](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) 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 }) => <Layout meta={meta} children={children} />
export default Page
@@ -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 }) => <Layout meta={meta} children={children} />
@@ -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).
<div className="video-container">
<iframe
@@ -143,7 +184,7 @@ For use-cases which require low-latency we recommend [Edge Functions](/docs/guid
We recommend developing “fat functions”. This means that you should develop few large functions, rather than many small functions. One common pattern when developing Functions is that you need to share code between two or more Functions. To do this, you can store any shared code in a folder prefixed with an underscore (`_`). We recommend this folder structure:
```bash
```txt
└── supabase
├── functions
│ ├── import_map.json # A top-level import map to use across functions.
+3 -3
View File
@@ -57,7 +57,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{quickstarts.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -73,7 +73,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{webapps.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -89,7 +89,7 @@ export const meta = {
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{mobile.map((item) => {
return (
<Link href={`/${item.href}`} key={item.title} passHref>
<Link href={`${item.href}`} key={item.title} passHref>
<a className={'col-span-4'}>
<GlassPanel title={item.title} span="col-span-6" background={false} icon={item.icon}>
{item.description}
@@ -6,12 +6,25 @@ export const meta = {
subtitle:
'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
breadcrumb: 'OpenAI',
video: 'https://www.youtube.com/v/xmfNUCjszh4',
tocVideo: 'xmfNUCjszh4',
}
In this tutorial we'll look at how you can build a custom ChatGPT-like search experience for your own knowledge base. See our [Supabase Clippy](https://supabase.com/blog/chatgpt-supabase-docs) blog post for an example of how this will look.
We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project.
## Video Guide
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/xmfNUCjszh4"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<Admonition type="note">
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)
+3 -3
View File
@@ -12,16 +12,16 @@ Explore a variety of integrations from Supabase partners. Need a different integ
<div>
{integrations.items.map((item) => (
<div>
<div key={item.name}>
<h2>{item.name}</h2>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 not-prose">
{item.items?.map((integration) => (
<Link href={`/${integration.url}`} key={integration.title} passHref>
<Link href={`${integration.url}`} key={integration.name} >
<a>
<GlassPanel
title={integration.name}
background={false}
icon={<Image height={40} width={40} className="rounded" src={`/docs/img/integrations/logos/${integration.name.toLowerCase()}_logo.png`}/>}
icon={<Image height={40} width={40} className="rounded" alt="" src={`/docs/img/integrations/logos/${integration.name.toLowerCase().replace(/ /g,"_")}_logo.png`}/>}
>
{integration.description}
@@ -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 }) => <Layout meta={meta} children={children} />
export default Page
+22 -1
View File
@@ -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:
@@ -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.
@@ -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 }) => <Layout meta={meta} children={children} />
export default Page
+1 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

+6
View File
@@ -552,6 +552,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/cloudflare-workers</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/integrations/dhiwise</loc>
<changefreq>weekly</changefreq>
@@ -12,16 +12,6 @@ date: '2022-06-30'
toc_depth: 3
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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.
@@ -11,16 +11,6 @@ date: '2022-08-02'
toc_depth: 3
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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!
+5 -1
View File
@@ -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
@@ -11,16 +11,6 @@ date: '2022-10-21'
toc_depth: 3
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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?
@@ -12,16 +12,6 @@ date: '2022-11-22'
toc_depth: 3
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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).
@@ -12,16 +12,6 @@ date: '2023-02-14'
toc_depth: 3
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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).
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+1
View File
@@ -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
@@ -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
@@ -10,16 +10,6 @@ date: '2023-05-04'
toc_depth: 2
---
<div className="bg-gray-300 rounded-lg px-6 py-2 bold">
🚨 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)
</div>
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.
@@ -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
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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
+81
View File
@@ -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.
<Quote img="michael-fester.jpeg" caption="Michael Fester - Co-Founder, Markprompt">
We’ve found that having access to the full features of Postgres, colocated with the embeddings,
makes it the perfect vector database.
</Quote>
## 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.
<Quote img="michael-fester.jpeg" caption="Michael Fester - Co-Founder, Markprompt">
We decided to use Supabase over other specialized vector databases because it enabled us to be
GDPR compliant from day one with little effort.
</Quote>
## 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.
<Quote img="marie-schneegans.jpeg" caption="Marie Schneegans - Co-Founder, Markprompt">
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.
</Quote>
## 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.
<Quote img="michael-fester.jpeg" caption="Michael Fester - Co-Founder, Markprompt">
Building everything on Supabase from the Auth, to DB, to the vectors—this integrated experience
really made it for us.
</Quote>
## 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
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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
+6 -2
View File
@@ -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 },
+52
View File
@@ -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 (
<div
className={[
'flex text-center gap-4 md:gap-2',
layout === 'horizontal'
? 'flex-col lg:flex-row items-center justify-center'
: 'flex-col items-center',
className,
].join(' ')}
>
<small className="small text-xs">backed by</small>
<div className="w-full sm:max-w-lg lg:ml-0">
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-8 sm:flex-nowrap">
<img
className="h-6 w-auto sm:h-8"
src={`${basePath}/images/logos/yc--grey.png`}
alt="Y Combinator"
/>
<img
className="relative w-auto h-4 sm:h-5"
src={`${basePath}/images/logos/mozilla--grey.png`}
alt="Mozilla"
/>
<img
className="relative w-auto h-4 sm:h-5"
src={`${basePath}/images/logos/coatue.png`}
alt="Coatue"
/>
<img
className="relative w-auto h-5 sm:h-6"
src={`${basePath}/images/logos/felicis.png`}
alt="Felicis"
/>
</div>
</div>
</div>
)
}
export default BackedBy
@@ -17,7 +17,7 @@ const BuiltExamples = () => {
<div className="flex justify-center gap-2 py-4">
<Link href="/docs/guides/examples" as="/docs/guides/examples" passHref>
<a>
<Button type="default" size="small">
<Button type="default" size="small" className="h-full">
View all examples
</Button>
</a>
@@ -125,6 +125,7 @@ function ImageCarousel(props: ImageCarouselProps) {
style={{ top: 0, left: 0 }}
frameBorder="0"
allow="autoplay; modestbranding; encrypted-media"
loading="lazy"
/>
</div>
)}
@@ -126,6 +126,7 @@ function CodeBlock(props: CodeBlockProps) {
)
}
onClick={() => handleCopy()}
aria-label="Copy"
>
{/* {copied ? 'Copied' : 'Copy'} */}
</Button>
+7 -3
View File
@@ -23,7 +23,7 @@ function ExampleCard(props: any) {
<img
src={props.author_img}
alt={props.author + ' GitHub profile picture'}
className="border-scale-500 inline w-6 rounded-full"
className="border-scale-500 inline w-6 h-6 rounded-full"
/>
<span className="text-scale-1200 ml-2 text-sm">{props.author}</span>
</div>
@@ -53,10 +53,14 @@ function ExampleCard(props: any) {
</a>
</Link>
<div className="mt-3 flex items-center gap-2">
<div className="mt-3 flex items-stretch gap-2 h-[26px]">
{props.vercel_deploy_url && (
<a target="_blank" href={props.vercel_deploy_url}>
<img className="h-6" src="https://vercel.com/button" alt="vercel button" />
<img
className="h-full w-auto"
src="https://vercel.com/button"
alt="vercel button"
/>
</a>
)}
{props.demo_url && (
+3 -3
View File
@@ -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 (
<div key={name} className="mb-10 space-y-4 md:mb-0">
<div className="flex items-center">
@@ -77,8 +77,8 @@ const Features = () => {
return (
<SectionContainer className="space-y-16 pb-0">
<h3 className="h3">Build faster and focus on your products</h3>
<dl className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 md:grid-cols-2 md:gap-16 lg:grid-cols-4 lg:gap-x-8 xl:gap-x-24">
<h3 className="h3 text-center">Build faster and focus on your products</h3>
<dl className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 md:grid-cols-2 md:gap-16 xl:grid-cols-5 lg:gap-x-8 xl:gap-x-12">
{IconSections}
</dl>
</SectionContainer>
-133
View File
@@ -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 (
<div className="overflow-hidden">
<SectionContainer className="pb-0 pt-24">
<div className="relative">
<main className="">
<div className="mx-auto">
<div className="lg:grid lg:grid-cols-12 lg:gap-16">
<div className="md:mx-auto md:max-w-2xl lg:col-span-6 lg:flex lg:items-center lg:text-left">
<div
className="
space-y-12 sm:mx-auto md:w-3/4 lg:mx-0
lg:w-full"
>
<div>
<h1
className="
text-scale-1200
text-2xl sm:text-3xl sm:leading-none lg:text-4xl
xl:text-5xl
"
>
<span className="block">Build in a weekend.</span>
<span className="text-brand-900 block md:ml-0">Scale to millions.</span>
</h1>
<div>
<p className="p mt-1.5 text-sm sm:mt-5 sm:text-base lg:text-lg ">
Supabase is an open source Firebase alternative. Start your project with a
Postgres database, Authentication, instant APIs, Edge Functions, Realtime
subscriptions, and Storage.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}>
<Button size="small" className="text-white">
Start your project
</Button>
</a>
</Link>
<Link href="/docs" as="/docs" passHref>
<a
onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])}
>
<Button size="small" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
</a>
</Link>
</div>
<div className="flex flex-col gap-4">
<small className="small text-xs">backed by</small>
<div className="w-full sm:max-w-lg lg:ml-0">
<div className="flex flex-wrap items-center justify-start gap-y-8 sm:flex-nowrap">
<img
className="h-8 pr-5 sm:h-8 md:pr-10"
src={`${basePath}/images/logos/yc--grey.png`}
alt="Y Combinator"
/>
<img
className="relative h-5 pr-5 sm:h-5 md:pr-10"
src={`${basePath}/images/logos/mozilla--grey.png`}
alt="Mozilla"
/>
<img
className="relative h-5 pr-5 sm:h-5 md:pr-10"
src={`${basePath}/images/logos/coatue.png`}
alt="Coatue"
/>
<img
className="relative h-6 pr-5 sm:h-6 md:pr-10"
src={`${basePath}/images/logos/felicis.png`}
alt="Felicis"
/>
</div>
</div>
</div>
</div>
</div>
<div className="mt-16 flex content-center sm:mt-24 lg:absolute lg:-right-80 lg:col-span-6 lg:mt-0 lg:w-9/12 xl:relative xl:right-0 xl:w-full">
<div className="relative flex w-full flex-col items-center justify-center rounded-md">
<div className="bg-scale-400 flex h-5 w-full items-center justify-start rounded-t-md px-2">
<div className="bg-scale-800 mr-2 h-2 w-2 rounded-full" />
<div className="bg-scale-800 mr-2 h-2 w-2 rounded-full" />
<div className="bg-scale-800 mr-2 h-2 w-2 rounded-full" />
</div>
<div
className="bg-scale-1000 relative w-full rounded-b-md shadow-lg"
style={{ padding: '56.25% 0 0 0' }}
>
<iframe
title="Demo video showcasing Supabase"
className="absolute h-full w-full rounded-b-md"
src="https://www.youtube-nocookie.com/embed/dBOSUER_5T4?playlist=dBOSUER_5T4&autoplay=1&loop=1&controls=0&modestbranding=1&rel=0&disablekb=1&mute=1&muted=1"
style={{ top: 0, left: 0 }}
frameBorder="0"
allow="autoplay; modestbranding; encrypted-media"
/>
</div>
{/*
Replace src with the following if have preference:
Vimeo: https://player.vimeo.com/video/485959063?loop=1&autoplay=1
Youtube: https://www.youtube-nocookie.com/embed/dBOSUER_5T4?playlist=dBOSUER_5T4&autoplay=1&loop=1&controls=0&modestbranding=1&rel=0&disablekb=1&mute=1&muted=1
*/}
</div>
</div>
</div>
</div>
</main>
</div>
</SectionContainer>
</div>
)
}
export default Hero
+71
View File
@@ -0,0 +1,71 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import gaEvents from '~/lib/gaEvents'
import { Button, IconBookOpen } from 'ui'
import SectionContainer from '~/components/Layouts/SectionContainer'
import HeroFrameworks from './HeroFrameworks'
import styles from './hero.module.css'
const Hero = () => {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const sendTelemetryEvent = async (event: TelemetryEvent) => {
await Telemetry.sendEvent(event, telemetryProps, router)
}
return (
<div className="relative">
<SectionContainer className="py-12 md:py-16 lg:py-20">
<div className="relative">
<div className="mx-auto">
<div className="mx-auto max-w-2xl lg:col-span-6 lg:flex lg:items-center justify-center text-center">
<div
className={[
'appear-first lg:h-[50vh] lg:min-h-[300px] lg:max-h-[450px] flex flex-col items-center justify-center sm:mx-auto md:w-3/4 lg:mx-0 lg:w-full gap-4 lg:gap-8',
styles['hero-text'],
].join(' ')}
>
<div>
<h1 className="text-scale-1200 text-4xl sm:text-5xl sm:leading-none lg:text-7xl">
<span className="block text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-scale-1200 to-scale-1200 dark:to-scale-1100">
Build in a weekend
</span>
<span className="text-transparent bg-clip-text bg-gradient-to-br from-[#3ECF8E] via-[#3ECF8E] to-[#3ecfb2] block md:ml-0">
Scale to millions
</span>
</h1>
<p className="pt-2 text-scale-1200 my-3 text-sm sm:mt-5 lg:mb-0 sm:text-base lg:text-lg">
Supabase is an open source Firebase alternative for building secure and
performant Postgres backends with minimal configuration.
</p>
</div>
<div className="flex items-center gap-2">
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}>
<Button size="medium" className="text-white">
Start your project
</Button>
</a>
</Link>
<Link href="/docs" as="/docs" passHref>
<a onClick={() => sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])}>
<Button size="medium" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
</a>
</Link>
</div>
<HeroFrameworks className="mt-4 lg:mt-6" />
</div>
</div>
</div>
</div>
</SectionContainer>
<div className="w-1/2 container mx-auto h-px bg-gradient-to-r from-transparent via-scale-600 to-transparent" />
</div>
)
}
export default Hero
+107
View File
@@ -0,0 +1,107 @@
import React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import ReactTooltip from 'react-tooltip'
import Telemetry from '~/lib/telemetry'
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
import { useRouter } from 'next/router'
const frameworks = [
{
name: 'Next.js',
icon: '/images/logos/frameworks/nextjs.svg',
docs: '/docs/guides/getting-started/quickstarts/nextjs',
gaEvent: 'www_hp_hero_frameworks_nextjs',
},
{
name: 'React',
icon: '/images/logos/frameworks/react.svg',
docs: '/docs/guides/getting-started/quickstarts/reactjs',
gaEvent: 'www_hp_hero_frameworks_react',
},
{
name: 'Nuxt',
icon: '/images/logos/frameworks/nuxtjs.svg',
docs: '/docs/guides/getting-started/quickstarts/nuxtjs',
gaEvent: 'www_hp_hero_frameworks_nuxt',
},
{
name: 'Flutter',
icon: '/images/logos/frameworks/flutter.svg',
docs: '/docs/guides/getting-started/quickstarts/flutter',
gaEvent: 'www_hp_hero_frameworks_flutter',
},
{
name: 'Svelte',
icon: '/images/logos/frameworks/svelte.svg',
docs: '/docs/guides/getting-started/quickstarts/sveltekit',
gaEvent: 'www_hp_hero_frameworks_svelte',
},
{
name: 'Python',
icon: '/images/logos/languages/python-icon.svg',
docs: '/docs/reference/python/introduction',
gaEvent: 'www_hp_hero_frameworks_python',
},
{
name: 'Vue',
icon: '/images/logos/frameworks/vue.svg',
docs: '/docs/guides/getting-started/quickstarts/vue',
gaEvent: 'www_hp_hero_frameworks_vue',
},
]
const HeroFrameworks = ({ className }: { className?: string }) => {
const router = useRouter()
const telemetryProps = useTelemetryProps()
const sendTelemetryEvent = async (gaEvent: string) => {
return await Telemetry.sendEvent(
{
action: gaEvent,
category: 'link',
label: '',
},
telemetryProps,
router
)
}
return (
<div className={['flex text-center flex-col items-center', className].join(' ')}>
<small className="small !text-scale-1100">Works seamlessly with 20+ frameworks</small>
<div className="w-full sm:max-w-lg mt-4 md:mt-3 lg:ml-0">
<div className="flex flex-wrap items-center justify-center gap-2 sm:flex-nowrap">
{frameworks.map((framework) => (
<Link href={framework.docs} key={framework.name}>
<a
key={framework.name}
className="m-0"
data-tip={framework.name}
onClick={() => sendTelemetryEvent(framework.gaEvent)}
>
<Image
key={framework.name}
className="opacity-100 hover:opacity-80 transition-opacity"
src={framework.icon}
alt={framework.name}
width={45}
height={45}
/>
</a>
</Link>
))}
<ReactTooltip
effect={'solid'}
place="bottom"
backgroundColor="#2e2e2e"
textColor="white"
className="!py-2 !px-4"
/>
</div>
</div>
</div>
)
}
export default HeroFrameworks
+12
View File
@@ -0,0 +1,12 @@
.hero-text {
opacity: 0;
transform: translate3d(0, 10px, 0);
animation: animateText 1s cubic-bezier(0.25, 0.25, 0, 1) 0.1s both;
}
@keyframes animateText {
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@@ -0,0 +1,64 @@
import React, { PropsWithChildren, useEffect, useRef } from 'react'
const InteractiveShimmerCard = ({
outerClassName,
innerClassName,
children,
}: PropsWithChildren<{ outerClassName?: string; innerClassName?: string }>) => {
const outerRef = useRef(null)
const innerRef = useRef(null)
const handleGlow = (event: any) => {
if (!outerRef.current || !innerRef.current) return null
const outerElement = outerRef.current as HTMLDivElement
const innerElement = innerRef.current as HTMLDivElement
let x: any
let y: any
const { x: elX, y: elY, width, height } = outerElement.getBoundingClientRect()
x = event.clientX - elX
y = event.clientY - elY
const isActive = x > -3 && x < width + 3 && y > -3 && y < height + 3
const activeGlow = isActive
? `radial-gradient(65rem circle at ${x}px ${y}px, var(--colors-brand9), transparent), `
: ''
outerElement.style.background = isActive ? `var(--colors-brand9)` : `var(--colors-scale3)`
outerElement.style.backgroundImage = `
${activeGlow}radial-gradient(30rem circle at ${x}px ${y}px, var(--colors-scale9), var(--colors-scale6))`
innerElement.style.backgroundImage = isActive
? `radial-gradient(7rem circle at ${x}px ${y}px, var(--colors-scale5), transparent), radial-gradient(20rem circle at ${x}px ${y}px, var(--colors-scale4), transparent)`
: ''
}
useEffect(() => {
if (typeof window === 'undefined') return
window.addEventListener('mousemove', handleGlow)
return () => {
window.removeEventListener('mousemove', handleGlow)
}
}, [])
return (
<div
ref={outerRef}
className={[
'relative rounded-xl bg-slate-400 from-slate-800 to-slate-800 p-px transition-all shadow-md',
outerClassName,
].join(' ')}
>
<div
className={[
'relative rounded-xl bg-scale-300 overflow-hidden transition-all flex flex-col text-slate-1100 lg:items-center lg:text-center',
innerClassName,
].join(' ')}
>
<div className="relative z-10 w-full h-full">{children}</div>
<div ref={innerRef} className="absolute z-0 inset-0 w-full h-full" />
</div>
</div>
)
}
export default InteractiveShimmerCard
+1 -1
View File
@@ -209,7 +209,7 @@ const Nav = () => {
/>
<nav
className={[
`border-scale-400 border-b backdrop-blur-sm transition-opacity`,
`border-scale-300 border-b backdrop-blur-sm transition-opacity`,
showLaunchWeekNavMode && '!opacity-100 !border-[#e0d2f430]',
].join(' ')}
>
+11 -5
View File
@@ -130,15 +130,21 @@ function BlogPostPage(props: any) {
</div>
)
const meta = {
title: props.blog.meta_title ?? props.blog.title,
description: props.blog.meat_description ?? props.blog.description,
url: `https://supabase.com/blog/${props.blog.slug}`,
}
return (
<>
<NextSeo
title={props.blog.title}
description={props.blog.description}
title={meta.title}
description={meta.description}
openGraph={{
title: props.blog.title,
description: props.blog.description,
url: `https://supabase.com/blog/${props.blog.slug}`,
title: meta.title,
description: meta.description,
url: meta.url,
type: 'article',
videos: props.blog.video && [
{
+2 -2
View File
@@ -62,8 +62,8 @@ function CaseStudyPage(props: any) {
const { basePath } = useRouter()
const meta = {
title: `${props.blog.name} | Supabase Customer Stories`,
description: props.blog.description,
title: props.blog.meta_title ?? `${props.blog.name} | Supabase Customer Stories`,
description: props.blog.meta_description ?? props.blog.description,
image: props.blog.og_image ?? `${basePath}/images/customers/og/customer-stories.jpg`,
url: `https://supabase.io/customers/${props.blog.slug}`,
}
+23 -25
View File
@@ -1,38 +1,36 @@
import { getSortedPosts, getAllCategories } from '~/lib/posts'
import dynamic from 'next/dynamic'
import { getSortedPosts } from '~/lib/posts'
import PostTypes from '~/types/post'
import Container from 'components/Container'
import Layout from '~/components/Layouts/Default'
import Hero from 'components/Hero'
import Features from 'components/Features/index'
import BuiltExamples from 'components/BuiltWithSupabase/index'
import MadeForDevelopers from 'components/MadeForDevelopers/index'
import AdminAccess from 'components/AdminAccess/index'
import CTABanner from 'components/CTABanner/index'
import CustomerStories from 'components/CustomerStories'
import TwitterSocialSection from '~/components/TwitterSocialSection'
import Hero from '~/components/Hero/Hero'
// Import Swiper styles if swiper used on page
import 'swiper/swiper.min.css'
const Features = dynamic(() => import('~/components/Features/index'))
const BackedBy = dynamic(() => import('~/components/BackedBy'))
const BuiltExamples = dynamic(() => import('components/BuiltWithSupabase/index'))
const MadeForDevelopers = dynamic(() => import('components/MadeForDevelopers/index'))
const AdminAccess = dynamic(() => import('components/AdminAccess/index'))
const CTABanner = dynamic(() => import('components/CTABanner/index'))
const CustomerStories = dynamic(() => import('components/CustomerStories'))
const TwitterSocialSection = dynamic(() => import('~/components/TwitterSocialSection'))
type Props = { customerStories: PostTypes[] }
const Index = ({ customerStories }: Props) => {
return (
<>
<Layout>
<Container>
<Hero />
<Features />
<TwitterSocialSection />
<BuiltExamples />
<MadeForDevelopers />
<AdminAccess />
<CustomerStories customerStories={customerStories} />
<CTABanner />
</Container>
</Layout>
</>
<Layout>
<Hero />
<Features />
<BackedBy className="pt-8 sm:pb-18 pb-16 md:pb-24 lg:pb-24" />
<TwitterSocialSection />
<BuiltExamples />
<MadeForDevelopers />
<AdminAccess />
<CustomerStories customerStories={customerStories} />
<CTABanner />
</Layout>
)
}
+2 -2
View File
@@ -9,9 +9,9 @@
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/customers/mendableai</guid>
<title>Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.</title>
<title>Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings.</title>
<link>https://supabase.com/customers/mendableai</link>
<description>How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.</description>
<description>How Mendable boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector.</description>
<pubDate>Thu, 04 May 2023 22:10:00 +0000</pubDate>
</item>
<item>
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

@@ -0,0 +1,3 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.9116 15.3209L35.9043 10.3135C35.2524 9.65849 33.8909 8.875 32.7414 8.875C31.7524 8.875 30.7827 9.07408 30.1533 9.44978L18.6131 15.2199H39.8435C40.2045 15.2199 40.562 15.2541 40.9116 15.3209ZM15.6432 17.8274L9.45173 30.7261C9.20448 31.2495 8.87695 32.1582 8.87695 32.7394C8.87695 33.995 9.42925 35.2794 10.3155 36.1913L15.6753 41.5111C15.654 41.3138 15.6432 41.1147 15.6432 40.9144V17.8274ZM20.7178 46.5904L26.4221 52.2947L26.7111 52.5837H44.5387H44.8277V46.6601H44.8259H21.6433C21.3316 46.6601 21.0223 46.6365 20.7178 46.5904ZM46.0424 44.8194H52.592V44.8001V27.3993L42.9754 17.7827C42.1448 16.952 41.0182 16.4854 39.8435 16.4854H17.5301L17.8007 16.7543L45.718 44.4969L46.0424 44.8194ZM18.1682 43.9552C19.0899 44.8768 20.3399 45.3946 21.6433 45.3946H43.0306H44.8259L43.5525 44.1291L18.1742 18.9096L16.9087 17.652V19.4361V40.9144C16.9087 42.0549 17.3618 43.1487 18.1682 43.9552Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,3 @@
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.4993 25.7267C45.1641 25.261 44.713 24.8888 44.1897 24.6461C43.6649 24.4025 43.0858 24.2961 42.5075 24.337C41.9293 24.3779 41.3713 24.5648 40.887 24.8798C40.4049 25.1938 40.0133 25.6259 39.7501 26.1341C39.4914 26.6796 39.4081 27.2905 39.5116 27.8843C39.6151 28.4781 39.9004 29.0262 40.3289 29.4545C41.5021 29.5461 42.6699 29.2198 43.6213 28.5346C44.5688 27.8532 45.2367 26.8559 45.4993 25.7267ZM35.5054 12.9135L33.3646 11.7629L24.0484 16.8458L24.7911 17.24L26.204 17.9672L28.7612 16.5742L35.5069 12.8901L35.5054 12.9135ZM36.3676 12.563C36.4226 12.5766 36.4732 12.6038 36.5146 12.6422C36.5559 12.6805 36.5867 12.7287 36.6039 12.7821L39.725 21.809C39.7573 21.8805 39.7599 21.9617 39.7323 22.0352C39.7047 22.1086 39.6491 22.1684 39.5774 22.2018C38.3894 22.8394 37.454 23.855 36.9228 25.0842C36.3939 26.3099 36.3027 27.6772 36.6644 28.961C37.0984 30.3611 38.0154 31.5661 39.2555 32.3662C40.4987 33.1693 41.9898 33.5197 43.4692 33.3591C43.5501 33.3544 43.6305 33.3742 43.6996 33.416C43.7688 33.4577 43.8233 33.5194 43.856 33.5927L47.0805 42.8576C47.1012 42.9322 47.0979 43.0113 47.0712 43.0841C47.0445 43.1568 46.9957 43.2195 46.9314 43.2636L37.0217 48.9437C36.9745 48.9634 36.9238 48.9734 36.8726 48.9729C36.7811 48.9896 36.6865 48.9741 36.6054 48.9291L33.1284 46.7899C33.0651 46.7575 33.0136 46.7065 32.9808 46.6439L26.1892 31.3367L15.8469 37.1045C15.785 37.1353 15.7171 37.1525 15.6478 37.1551C15.5785 37.1576 15.5094 37.1453 15.4454 37.1191L13.0949 35.8078C13.0187 35.7673 12.961 35.6996 12.9335 35.6185C12.9061 35.5373 12.911 35.4489 12.9473 35.3712L22.9943 16.6808C23.0319 16.6171 23.0887 16.5665 23.1567 16.5362L33.1742 11.059C33.229 11.0305 33.2901 11.0156 33.3521 11.0156C33.4141 11.0156 33.4751 11.0305 33.53 11.059L36.3691 12.563H36.3676ZM23.9451 17.5846L23.4579 17.3379L13.7726 35.415L15.5399 36.3904L23.8919 25.6799C23.9329 25.6305 23.9853 25.5915 24.0446 25.5661C24.1039 25.5407 24.1685 25.5297 24.233 25.5339C24.296 25.5452 24.3556 25.5706 24.4071 25.6084C24.4586 25.6461 24.5005 25.6951 24.5297 25.7515L33.6245 46.2905L36.0546 47.761L26.2926 19.7837L25.8777 18.6185L23.9318 17.57L23.9466 17.5846H23.9451ZM38.9233 28.3638C38.7009 27.558 38.7634 26.7013 39.1005 25.9355C39.439 25.1661 40.0313 24.5329 40.7807 24.1394C41.5429 23.7223 42.419 23.5541 43.2838 23.659C44.1485 23.7639 44.9575 24.1365 45.5952 24.7235C45.95 25.0575 46.2345 25.4577 46.4321 25.901C46.6297 26.3443 46.7366 26.8219 46.7466 27.3063C46.7566 27.7907 46.6696 28.2722 46.4904 28.7231C46.3113 29.174 46.0437 29.5854 45.703 29.9335L45.5952 30.0401C44.8392 30.7444 43.8405 31.1379 42.8018 31.1406C41.7632 31.1433 40.7624 30.7551 40.0026 30.0547C39.5029 29.5913 39.1313 29.0092 38.9233 28.3638Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,4 @@
<svg width="62" height="61" viewBox="0 0 62 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.5067 10.3828L34.3509 10.3962L14.75 29.9971L20.7974 36.0519L26.1125 30.7666L46.5067 10.3828Z" fill="#7E7E7E"/>
<path d="M34.6996 28.4653C34.5272 28.4573 34.3493 28.4491 34.2378 28.5965L23.7856 39.0471L29.7894 45.0142L29.7825 45.021L34.079 49.3212C34.1072 49.3462 34.1352 49.3741 34.1637 49.4026C34.2813 49.5201 34.4074 49.6462 34.5895 49.6055C36.5743 49.601 38.5591 49.6017 40.544 49.6025C42.529 49.6032 44.5142 49.604 46.4998 49.5995L35.9333 39.0234L46.4963 28.467L34.906 28.464C34.8415 28.4719 34.7711 28.4686 34.6996 28.4653Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 662 B

@@ -0,0 +1,3 @@
<svg width="61" height="62" viewBox="0 0 61 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.7142 12.6406C45.7879 12.6406 46.7818 13.2175 47.2853 14.1553C47.7677 15.0536 47.7472 16.1121 47.2318 16.9907L47.2142 17.0202L42.6965 24.5242C42.1856 25.3728 41.2633 25.9024 40.2775 25.9137L40.2453 25.9139L35.0864 25.9138L37.2306 30.7881L37.2392 30.8023L37.2524 30.8254C37.7573 31.7316 37.745 32.8091 37.2213 33.7018L37.2037 33.7315L32.6859 41.2355C32.175 42.084 31.2527 42.6136 30.267 42.6249L30.2349 42.6251L23.9028 42.6251L19.3509 47.3922L19.3423 47.401C18.9139 47.8334 18.3352 48.0746 17.7318 48.0746C17.5809 48.0746 17.43 48.0594 17.2806 48.0292C16.5446 47.88 15.933 47.3785 15.6387 46.6868L15.6304 46.6671L13.2349 41.207L13.2223 41.1861L13.2092 41.1635C13.207 41.1597 13.2048 41.1557 13.2025 41.1516C12.6975 40.2454 12.7097 39.168 13.2334 38.2752L13.251 38.2455L17.6328 30.9674L14.4041 23.628L14.4632 23.5981L14.4623 23.5937C14.3257 22.9186 14.4357 22.2146 14.7865 21.5979L14.8059 21.5644L14.8238 21.5342L19.3416 14.0302C19.8526 13.1817 20.7749 12.6521 21.7606 12.6406H21.7927H44.7142ZM20.9923 42.568L16.1921 42.568L17.602 45.8062L17.6055 45.8148C17.6288 45.8728 17.6666 45.9046 17.7229 45.9161C17.7731 45.9264 17.8137 45.9158 17.8521 45.881L17.8588 45.8745L20.9923 42.568ZM34.7145 31.435H20.2047C20.1887 31.435 20.1727 31.4355 20.1567 31.4366L20.1523 31.4369L24.1307 40.4731H30.2341C30.485 40.4731 30.7267 40.3397 30.8641 40.1255L30.875 40.1079L35.3993 32.5993C35.5343 32.3754 35.5492 32.1155 35.4423 31.8816C35.319 31.612 35.0328 31.435 34.7145 31.435ZM18.6964 33.2905L15.1056 39.3C14.9627 39.5391 14.9545 39.8193 15.0824 40.0658L15.096 40.0908L15.1032 40.1031L15.1177 40.1266L15.1464 40.1691L15.1785 40.2105L15.2091 40.2453L15.2145 40.2509L15.2352 40.2717L15.2532 40.2888C15.3662 40.3906 15.5058 40.4527 15.6598 40.4687L15.6944 40.4716L15.7144 40.4726L15.7398 40.4731H21.8323L18.6964 33.2905ZM17.7522 25.9284L19.3292 29.5196L19.36 29.5092C19.6323 29.4188 19.9171 29.3697 20.2049 29.3652L20.2481 29.3649L34.3128 29.3649L32.8038 25.9284L17.7522 25.9284ZM44.6742 14.7355H30.1126L34.0923 23.7736H40.1935C40.4448 23.7736 40.6867 23.6401 40.8242 23.4258L40.8351 23.4083L45.3597 15.8998C45.4946 15.6759 45.5096 15.4162 45.4027 15.1826C45.2819 14.9184 45.005 14.7431 44.6945 14.7358L44.6742 14.7355ZM27.8835 14.7355H21.8161C21.5665 14.7355 21.3259 14.8687 21.189 15.0827L21.1781 15.1002L16.6654 22.6088C16.5307 22.8329 16.5158 23.0932 16.6226 23.3273C16.743 23.5912 17.0185 23.766 17.3271 23.7734L17.3473 23.7736H31.8527L27.8835 14.7355Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,3 @@
<svg width="61" height="62" viewBox="0 0 61 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.3148 48.6796C38.9009 50.9525 34.8014 52.2771 30.3924 52.2771C18.4957 52.2771 8.85156 42.6329 8.85156 30.7362C8.85156 18.8395 18.4957 9.19531 30.3924 9.19531C42.2891 9.19531 51.9333 18.8395 51.9333 30.7362C51.9333 37.1564 49.1245 42.9207 44.6688 46.8671L39.5552 40.2803V21.8278H36.584V36.4531L25.2299 21.8278H21.4808V39.6473H24.4801V25.6368L42.3148 48.6796Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

@@ -0,0 +1,3 @@
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.5784 45.4741H50.2199C50.7802 45.4741 51.3307 45.3325 51.8159 45.0634C52.3012 44.7943 52.7041 44.4072 52.9842 43.9409C53.2642 43.4748 53.4115 42.946 53.4113 42.4078C53.4111 41.8696 53.2633 41.3409 52.9828 40.875L41.1352 21.164C40.8552 20.6979 40.4524 20.3109 39.9672 20.0418C39.4821 19.7727 38.9317 19.631 38.3715 19.631C37.8113 19.631 37.261 19.7727 36.7758 20.0418C36.2906 20.3109 35.8878 20.6979 35.6078 21.164L32.5784 26.2073L26.6555 16.3452C26.3753 15.8792 25.9723 15.4922 25.487 15.2232C25.0017 14.9541 24.4513 14.8125 23.8909 14.8125C23.3306 14.8125 22.7802 14.9541 22.2949 15.2232C21.8096 15.4922 21.4066 15.8792 21.1263 16.3452L6.38358 40.875C6.10311 41.3409 5.95532 41.8696 5.95508 42.4078C5.95483 42.946 6.10214 43.4748 6.38219 43.9409C6.66224 44.4072 7.06515 44.7943 7.5504 45.0634C8.03564 45.3325 8.58612 45.4741 9.14645 45.4741H20.2203C24.6079 45.4741 27.8436 43.6229 30.07 40.0113L38.3706 26.2073L47.0599 40.6619H35.4754L32.5784 45.4741ZM20.0398 40.657L12.3116 40.6553L23.8961 21.3836L29.6763 31.0195L25.8062 37.4599C24.3276 39.8032 22.6479 40.657 20.0398 40.657Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.74 23.6983C45.2739 23.5379 44.7909 23.3861 44.2937 23.2426C44.3754 22.909 44.4504 22.5798 44.5171 22.2561C45.6119 16.9418 44.8961 12.6605 42.4518 11.2509C40.1079 9.89927 36.2748 11.3085 32.4035 14.6776C32.0313 15.0016 31.6579 15.3446 31.2848 15.704C31.0362 15.4662 30.7879 15.2364 30.5403 15.0165C26.4831 11.4141 22.4164 9.89599 19.9744 11.3097C17.6329 12.6652 16.9394 16.69 17.9249 21.7265C18.0201 22.2129 18.1313 22.7097 18.2571 23.2148C17.6816 23.3782 17.1259 23.5524 16.5943 23.7377C11.8376 25.3961 8.7998 27.9952 8.7998 30.6911C8.7998 33.4755 12.0609 36.2683 17.0153 37.9617C17.4063 38.0953 17.812 38.2217 18.2301 38.3416C18.0944 38.8879 17.9763 39.4232 17.8773 39.9454C16.9376 44.8944 17.6714 48.8242 20.0068 50.1711C22.4189 51.5622 26.4673 50.1324 30.4093 46.6865C30.7209 46.4141 31.0336 46.1253 31.3469 45.8225C31.7529 46.2135 32.1582 46.5835 32.5615 46.9306C36.3798 50.2164 40.151 51.5432 42.4842 50.1925C44.894 48.7975 45.6772 44.576 44.6604 39.4399C44.5828 39.0476 44.4924 38.6469 44.3909 38.239C44.6752 38.155 44.9543 38.0682 45.2265 37.978C50.3771 36.2715 53.7282 33.5127 53.7282 30.6911C53.7282 27.9854 50.5924 25.3688 45.74 23.6983ZM44.6228 36.1561C44.3772 36.2375 44.1251 36.3161 43.8682 36.3923C43.2996 34.5922 42.5322 32.6781 41.5931 30.7005C42.4893 28.7699 43.227 26.8803 43.7797 25.0919C44.2393 25.2249 44.6854 25.3651 45.1152 25.5132C49.2728 26.9444 51.8089 29.0605 51.8089 30.6911C51.8089 32.4279 49.07 34.6826 44.6228 36.1561ZM42.7776 39.8126C43.2272 42.0837 43.2914 44.1371 42.9936 45.7423C42.726 47.1847 42.1878 48.1463 41.5225 48.5315C40.1066 49.351 37.0787 48.2857 33.8132 45.4757C33.4388 45.1535 33.0618 44.8096 32.6835 44.4455C33.9495 43.061 35.2147 41.4514 36.4495 39.6638C38.6215 39.4711 40.6735 39.156 42.5344 38.7258C42.626 39.0955 42.7074 39.4581 42.7776 39.8126ZM24.1169 48.3898C22.7336 48.8784 21.6318 48.8924 20.9658 48.5084C19.5486 47.691 18.9594 44.5358 19.7631 40.3033C19.8551 39.8186 19.9647 39.3207 20.091 38.8118C21.9314 39.2187 23.9684 39.5116 26.1456 39.6881C27.3887 41.4373 28.6905 43.0452 30.0024 44.453C29.7157 44.7297 29.4302 44.9931 29.1463 45.2413C27.4032 46.7651 25.6564 47.8461 24.1169 48.3898ZM17.6361 36.1455C15.4453 35.3967 13.6361 34.4235 12.396 33.3616C11.2817 32.4073 10.7191 31.4599 10.7191 30.6911C10.7191 29.0551 13.1581 26.9684 17.226 25.5501C17.7196 25.378 18.2363 25.2158 18.7725 25.0635C19.3347 26.8923 20.0722 28.8043 20.9623 30.7378C20.0607 32.7 19.3128 34.6425 18.745 36.4927C18.3628 36.3829 17.9924 36.2672 17.6361 36.1455ZM19.8085 21.3579C18.9642 17.0428 19.5249 13.7876 20.936 12.9708C22.4391 12.1006 25.7628 13.3413 29.2659 16.4518C29.4898 16.6506 29.7146 16.8587 29.9401 17.074C28.6347 18.4756 27.3448 20.0714 26.1127 21.8103C23.9997 22.0061 21.977 22.3208 20.1174 22.742C20.0004 22.2717 19.8969 21.8097 19.8085 21.3579ZM39.1886 26.1433C38.744 25.3754 38.2876 24.6257 37.8223 23.8964C39.2558 24.0777 40.6293 24.3182 41.9191 24.6126C41.5318 25.8536 41.0492 27.1511 40.4811 28.4813C40.0735 27.7076 39.6425 26.9275 39.1886 26.1433ZM31.2854 18.4456C32.1707 19.4047 33.0573 20.4756 33.9293 21.6374C33.0506 21.5959 32.161 21.5743 31.264 21.5743C30.3755 21.5743 29.4925 21.5954 28.6192 21.6362C29.4921 20.4852 30.3863 19.4158 31.2854 18.4456ZM23.3317 26.1566C22.8876 26.9267 22.4645 27.7025 22.0634 28.4799C21.5045 27.1543 21.0263 25.8509 20.6357 24.5923C21.9176 24.3054 23.2846 24.0709 24.7089 23.8931C24.2371 24.6291 23.7769 25.3843 23.3317 26.1564V26.1566ZM24.75 37.626C23.2783 37.4618 21.8908 37.2394 20.6093 36.9604C21.0061 35.6793 21.4948 34.3481 22.0655 32.994C22.4677 33.7707 22.8925 34.5469 23.3393 35.3187H23.3393C23.7945 36.1049 24.266 36.875 24.75 37.626ZM31.3385 43.0719C30.4289 42.0904 29.5215 41.0047 28.6353 39.8368C29.4956 39.8706 30.3726 39.8879 31.264 39.8879C32.1798 39.8879 33.085 39.8672 33.9761 39.8276C33.1012 41.0164 32.2178 42.1038 31.3385 43.0719ZM40.4994 32.9249C41.0999 34.2937 41.6061 35.618 42.0081 36.8772C40.7054 37.1744 39.2989 37.4138 37.8171 37.5916C38.2835 36.8525 38.7439 36.0899 39.1963 35.3055C39.6539 34.5118 40.0885 33.717 40.4994 32.9249ZM37.5337 34.3466C36.8314 35.5643 36.1104 36.7268 35.3784 37.8241C34.0452 37.9194 32.6678 37.9685 31.264 37.9685C29.8659 37.9685 28.5058 37.9251 27.1962 37.8401C26.4347 36.7284 25.698 35.5625 25.0002 34.3571H25.0004C24.3044 33.155 23.6638 31.9427 23.0834 30.7372C23.6636 29.5289 24.3025 28.3152 24.9945 27.1152L24.9944 27.1155C25.6882 25.9123 26.4184 24.7521 27.1729 23.6473C28.509 23.5463 29.8792 23.4936 31.2639 23.4936H31.264C32.655 23.4936 34.0269 23.5467 35.3626 23.6486C36.1056 24.7453 36.8308 25.9017 37.5274 27.1051C38.2319 28.3219 38.879 29.5275 39.4642 30.7099C38.8808 31.9126 38.2351 33.1303 37.5337 34.3466ZM41.4931 12.9137C42.9976 13.7813 43.5826 17.2804 42.6374 21.8688C42.5771 22.1615 42.5092 22.4597 42.4354 22.762C40.5715 22.3319 38.5474 22.0118 36.4282 21.813C35.1937 20.055 33.9143 18.4567 32.6302 17.0731C32.9755 16.741 33.3202 16.4243 33.6636 16.1254C36.9805 13.2388 40.0806 12.0991 41.4931 12.9137ZM31.264 26.6791C33.Line truncated
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

@@ -0,0 +1,3 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.3228 12.4486C29.3244 12.4447 29.3261 12.4408 29.3277 12.4369C29.3277 12.4369 28.7975 11.9228 28.4601 11.6979C26.6287 10.4769 23.8976 10.0752 22.2429 11.7943C21.9556 12.0909 21.695 12.4035 21.4365 12.7137C21.2469 12.9412 21.0584 13.1673 20.8613 13.3848C20.3312 13.9632 19.6886 14.4452 18.9496 14.7183C18.5352 14.8737 18.1209 14.9088 17.7022 14.9443C17.4716 14.9639 17.2397 14.9836 17.0057 15.0235C16.331 15.136 15.6402 15.377 15.094 15.8108C14.4995 16.2927 14.2586 16.9354 14.1943 17.6744C14.1542 18.0354 14.1454 18.4028 14.1366 18.7685C14.1313 18.9887 14.1261 19.2083 14.114 19.4256C14.0337 21.1286 13.4874 21.6105 12.2665 22.5584C11.7524 22.944 11.3187 23.4581 10.9973 24.0044C10.388 25.081 10.7802 26.1833 11.1715 27.2828C11.3992 27.9225 11.6265 28.5613 11.656 29.1936C11.6721 29.5471 11.64 29.9005 11.5114 30.2379C11.4215 30.4963 11.2688 30.6919 11.1136 30.8906C11.0469 30.9759 10.9798 31.0619 10.917 31.1537C10.6279 31.5553 10.4351 32.0373 10.5154 32.5353C10.5957 33.0334 10.8528 33.4993 11.0937 33.9331C11.5596 34.7042 12.138 35.395 12.7484 36.0538C12.7742 36.0409 12.7999 36.0692 12.8256 36.0974L12.8266 36.0985C13.3984 37.891 14.2536 39.6166 15.3507 41.2589C19.5276 47.4925 26.3071 49.6453 26.3071 49.6453C36.3478 53.0513 43.2076 48.0387 43.2076 48.0387C49.5212 43.701 50.7903 38.6884 50.7903 38.6884C45.6977 42.6567 43.4968 42.2711 43.4968 42.2711C47.1917 40.6645 48.7661 37.8851 48.7661 37.8851C52.2201 33.1778 51.0474 28.2135 51.0474 28.2135C48.8465 33.0011 46.8383 34.3185 46.8383 34.3185C52.2523 26.0767 46.8704 21.0963 46.8704 21.0963C49.0714 25.7875 46.1475 31.507 46.1475 31.507C43.6896 36.6802 40.2034 38.5277 40.2034 38.5277C44.0912 39.2507 46.9508 36.632 46.9508 36.632C41.3762 42.6567 35.3357 42.3675 35.3357 42.3675C37.874 45.2111 41.6814 44.9702 41.6814 44.9702C36.7012 46.143 32.0263 44.4882 28.1867 41.2911C27.4638 40.6966 26.773 40.054 26.1304 39.3632C26.1304 39.3632 25.552 38.7527 25.4396 38.592L25.4235 38.5759C25.3432 41.5481 28.4438 44.3115 28.4438 44.3115C24.556 42.7049 22.7728 39.2186 22.7728 39.2186C22.7728 39.2186 20.1542 34.7523 22.0498 29.6594C23.9455 24.5665 29.6808 23.474 29.6808 23.474C34.42 25.7714 38.4363 26.4944 38.4363 26.4944C46.9026 27.9082 46.4206 23.7632 46.4206 23.7632C46.5009 20.1966 41.1191 16.405 41.1191 16.405C36.7533 13.1726 32.5772 12.367 29.3228 12.4486ZM35.657 18.831C36.1389 18.831 36.5245 19.2166 36.5245 19.6985C36.5245 20.1805 36.1389 20.5661 35.657 20.5661C35.175 20.5661 34.7895 20.1805 34.7895 19.6985C34.7895 19.2166 35.175 18.831 35.657 18.831ZM30.163 17.2887L24.6848 15.5857C24.6787 15.5857 24.668 15.5828 24.6527 15.5787C24.5871 15.561 24.4372 15.5206 24.2028 15.5857C24.2028 15.5857 20.9737 16.405 21.536 18.1723C21.536 18.1723 21.8734 19.2808 22.7891 20.3572L28.8135 20.068L30.163 17.2887Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,3 @@
<svg width="62" height="61" viewBox="0 0 62 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.6705 16.2784L30.1948 21.3965C30.3645 21.5093 30.5629 21.5709 30.7665 21.5744C30.9704 21.573 31.1693 21.5111 31.3383 21.3965L38.8683 16.2612C39.1608 16.0525 39.3222 15.7045 39.2929 15.3454C39.2635 14.9863 39.0479 14.6693 38.7254 14.5112L31.201 10.7988C30.9156 10.6608 30.5831 10.6608 30.2976 10.7988L22.7963 14.5112C22.4679 14.6702 22.2494 14.9933 22.2234 15.3582C22.1974 15.7232 22.368 16.0742 22.6705 16.2784ZM33.3223 23.0031C33.3229 23.3426 33.4896 23.66 33.7682 23.8523L39.8002 27.9606C40.1864 28.2259 40.703 28.1928 41.0524 27.8803L46.1124 23.3703C46.3379 23.1695 46.4631 22.8788 46.4543 22.5764C46.4455 22.274 46.3035 21.9911 46.0667 21.8039L41.2354 17.9539C40.8841 17.6759 40.3943 17.6573 40.0232 17.908L33.7682 22.1711C33.4945 22.36 33.3284 22.6698 33.3223 23.0031ZM18.3194 28.5917C18.5632 28.81 18.6886 29.1319 18.6568 29.4582C18.6251 29.7863 18.4349 30.078 18.1479 30.2385L14.5459 32.3901C14.1807 32.6077 13.7197 32.5763 13.3871 32.3113C13.0546 32.0465 12.9196 31.6029 13.0479 31.1967L14.38 27.0196C14.4846 26.6886 14.7495 26.4336 15.0833 26.3426C15.4169 26.2457 15.7767 26.3306 16.0324 26.5663L18.3194 28.5917ZM37.8562 29.0278L31.344 24.5867C30.9979 24.3547 30.5466 24.3547 30.2005 24.5867L23.6882 29.0278C23.4292 29.2065 23.2651 29.4936 23.2423 29.8081C23.2254 30.125 23.3513 30.4325 23.5853 30.6459L30.0918 36.4468C30.2795 36.6136 30.5216 36.7054 30.7723 36.705C31.0227 36.7049 31.2648 36.6131 31.4526 36.4468L37.9592 30.6459C38.1938 30.4334 38.3182 30.1246 38.2965 29.8081C38.2783 29.4936 38.1157 29.2053 37.8562 29.0278ZM20.4864 27.8803L15.4321 23.3703C15.2047 23.1645 15.0792 22.8685 15.089 22.5613C15.0945 22.2586 15.2351 21.9742 15.4721 21.7867L20.3034 17.9137C20.657 17.6365 21.1479 17.618 21.5213 17.8678L27.7706 22.131C28.0605 22.3205 28.2355 22.6443 28.2355 22.9916C28.2355 23.339 28.0605 23.6628 27.7706 23.8523L21.7442 27.9605C21.3557 28.2247 20.8384 28.1917 20.4864 27.8803ZM46.4383 34.3582L41.2925 31.2885C40.9103 31.059 40.4239 31.1055 40.0918 31.4033L33.8026 36.9976C33.5372 37.2337 33.4126 37.5908 33.4732 37.9415C33.5338 38.2921 33.7708 38.5863 34.0998 38.719L42.8248 42.242C42.9445 42.2915 43.0727 42.3169 43.2022 42.3166C43.6071 42.3178 43.9747 42.0794 44.1398 41.7084L46.8443 35.6722C47.0643 35.1945 46.8891 34.6272 46.4383 34.3582ZM47.153 27.0196L48.4852 31.1967H48.4737C48.5723 31.5091 48.5163 31.8501 48.3231 32.1142C48.1299 32.3785 47.8226 32.5342 47.496 32.5337C47.3129 32.5342 47.133 32.4846 46.9757 32.3902L43.368 30.2385C43.0868 30.0747 42.9033 29.7833 42.8763 29.4582C42.8425 29.1316 42.9682 28.8089 43.2136 28.5917L45.5006 26.5605C45.759 26.3299 46.116 26.2456 46.4498 26.3368C46.7834 26.4317 47.0473 26.6882 47.153 27.0196ZM28.0678 37.9386C28.1294 37.5894 28.006 37.2333 27.7419 36.9976L21.4527 31.4033C21.1206 31.1055 20.6341 31.059 20.252 31.2885L15.1062 34.3582C14.6606 34.6284 14.4844 35.1886 14.6945 35.6665L17.4046 41.7027C17.6292 42.2059 18.2097 42.4415 18.7197 42.2363L27.439 38.7133C27.7681 38.5817 28.0059 38.2887 28.0678 37.9386ZM31.1553 39.66L38.1536 42.483C38.4958 42.6289 38.7322 42.9496 38.7711 43.3207C38.8154 43.6964 38.648 44.0657 38.3366 44.2789L31.3326 49.1273C31.1647 49.2448 30.9655 49.3088 30.7608 49.311C30.5564 49.3078 30.3574 49.2439 30.1891 49.1273L23.1908 44.2789C22.8783 44.0662 22.7088 43.6973 22.7506 43.3207C22.7973 42.9457 23.0421 42.6254 23.3909 42.483L30.3892 39.66C30.6352 39.5622 30.9093 39.5622 31.1553 39.66Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,4 @@
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.5941 39.7635C44.9249 43.9427 44.9249 45.9015 44.9249 48.0393H35.1061C35.1061 47.5729 35.1139 47.1478 35.1216 46.715C35.1496 45.3724 35.1776 43.9717 34.9555 41.1443C34.6635 37.0034 32.8525 36.0829 29.5242 36.0829H14.0801V28.5503H29.9902C34.196 28.5503 36.3005 27.2918 36.3005 23.9552C36.3005 21.0239 34.196 19.2455 29.9902 19.2455H14.0801V11.875H31.7436C41.2642 11.875 45.9965 16.3034 45.9965 23.3757C45.9965 28.6665 42.6667 32.1163 38.1689 32.6912C41.9662 33.439 44.1856 35.566 44.5941 39.7635Z" fill="#7E7E7E"/>
<path d="M14.0801 48.0393V42.4242H24.4626C26.1959 42.4242 26.5733 43.6904 26.5733 44.4458V48.0393H14.0801Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 754 B

@@ -0,0 +1,7 @@
<svg width="62" height="61" viewBox="0 0 62 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.6036 13.5595C35.9569 11.4125 48.0087 20.6009 48.0087 20.6009L43.9152 27.5037L43.8369 27.4882C43.8886 27.5276 43.9152 27.55 43.9152 27.55C43.9123 27.5445 43.6566 27.3469 43.1932 27.0197C40.4605 25.0901 30.5005 18.6515 22.51 20.4857C22.4711 20.4955 22.4317 20.5053 22.392 20.5152C22.1521 20.575 21.9026 20.6373 21.6867 20.7165C20.4651 21.1153 19.5009 21.755 18.8262 22.5442L18.8053 22.5401L22.2585 16.6069C22.3017 16.5371 22.3417 16.464 22.3821 16.39C22.4485 16.2684 22.5162 16.1446 22.6015 16.0298C23.2647 15.0601 24.3395 14.2521 25.7803 13.7904C25.9961 13.7112 26.2457 13.6489 26.4855 13.5891L26.4874 13.5886C26.5264 13.5789 26.5652 13.5692 26.6036 13.5595Z" fill="#7E7E7E"/>
<path d="M21.4546 30.3115L30.3998 27.3882C33.0987 26.5072 36.3229 27.0328 38.757 28.5331C38.7337 28.5172 39.4797 29.0111 39.5587 29.1001L42.9442 27.7685C42.6997 27.6012 41.8005 26.9483 41.4884 26.7449C40.2308 25.9258 38.4589 24.8586 36.4018 23.8593C32.2517 21.8432 27.0993 20.1828 22.6805 21.1939L22.5625 21.2235C22.3197 21.2842 22.1138 21.3357 21.9377 21.4003L21.9253 21.4049L21.9128 21.409C20.1779 21.9753 19.0916 23.0464 18.6521 24.2528C18.2145 25.4538 18.3771 26.8991 19.3579 28.3121C19.8933 29.0834 20.6135 29.7595 21.4546 30.3115Z" fill="#7E7E7E"/>
<path d="M37.1649 44.5327L37.1689 44.5413L37.1604 44.5398L37.1649 44.5327Z" fill="#7E7E7E"/>
<path d="M41.1088 35.9638L37.8597 41.8024C37.8009 40.8557 37.4574 39.8741 36.8031 38.9314C34.6077 35.7686 29.7366 34.1987 25.9176 35.4454L12.7197 39.7798L12.7818 39.5887L16.6279 32.6552L30.6259 28.0807C32.368 27.512 34.3819 27.5786 36.2289 28.1713C38.0772 28.7643 39.6975 29.8642 40.6868 31.2893C41.8269 32.9335 41.8703 34.6186 41.1088 35.9638Z" fill="#7E7E7E"/>
<path d="M14.0732 40.923C13.7652 40.7232 13.4883 40.5385 13.246 40.3736L26.1443 36.1376C27.8863 35.5692 29.8999 35.6358 31.7467 36.2284C33.5951 36.8215 35.2154 37.9215 36.2047 39.3468C37.1855 40.7598 37.3481 42.2051 36.9105 43.4061C36.471 44.6125 35.3847 45.6837 33.6499 46.25L33.6373 46.2541L33.6249 46.2586C33.4488 46.3232 33.2429 46.3747 33.0001 46.4355L32.8822 46.465C28.4633 47.4762 23.3108 45.8157 19.1605 43.8024C17.1032 42.8044 15.3312 41.7392 14.0732 40.923Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,3 @@
<svg width="62" height="61" viewBox="0 0 62 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8421 13.5737C33.6246 10.5267 40.239 11.9474 43.588 16.7408H43.5882C45.1937 18.9881 45.826 21.7874 45.3423 24.5066C45.1104 25.792 44.6203 27.017 43.9015 28.1076C44.9502 30.1077 45.3021 32.4004 44.9015 34.6229C44.4229 37.291 42.8426 39.634 40.5482 41.0772L32.1489 46.4304C27.3671 49.4774 20.7527 48.0572 17.4029 43.2634C15.7977 41.016 15.1654 38.2167 15.6488 35.4975C15.8809 34.2121 16.3713 32.987 17.0902 31.8964C16.041 29.8965 15.6888 27.6038 16.0895 25.3813C16.5683 22.7133 18.1485 20.3704 20.4428 18.927L28.8421 13.5737ZM20.8577 41.0643C22.4527 43.3635 25.3126 44.4236 28.0207 43.7194H28.0206C28.6247 43.558 29.2004 43.305 29.7278 42.9693L38.1291 37.6151C39.5092 36.7467 40.4598 35.3374 40.7479 33.7325C41.0388 32.0968 40.6583 30.4128 39.6925 29.061C38.0976 26.7618 35.2377 25.7017 32.5296 26.4059C31.926 26.5672 31.3506 26.8201 30.8237 27.1557L27.618 29.1987C27.4585 29.3002 27.2845 29.3766 27.1019 29.4255C26.286 29.6371 25.4247 29.3173 24.9444 28.6246C24.6537 28.2169 24.5393 27.7094 24.627 27.2164C24.7138 26.7331 25.0001 26.3087 25.4157 26.0471L33.8177 20.6927C33.9771 20.5912 34.1512 20.5147 34.3338 20.4659C35.1494 20.2542 36.0104 20.5738 36.4904 21.2664C36.7471 21.6298 36.8671 22.0722 36.8293 22.5155L36.8005 22.8272L37.1128 22.922C38.294 23.2783 39.4056 23.8339 40.3996 24.5648L40.8298 24.8804L40.988 24.398C41.0728 24.1413 41.1399 23.8792 41.1888 23.6134C41.4796 21.9776 41.0992 20.2937 40.1334 18.9419C38.5385 16.6427 35.6786 15.5826 32.9705 16.2868C32.3664 16.4481 31.7907 16.7011 31.2633 17.0369L22.862 22.3915C21.4818 23.2594 20.5311 24.6687 20.2432 26.2735C19.9523 27.9092 20.3328 29.5932 21.2987 30.945C22.8935 33.2442 25.7534 34.3043 28.4616 33.6001C29.0651 33.4388 29.6405 33.1861 30.1677 32.8509L33.3738 30.8071C33.533 30.7057 33.7068 30.6293 33.8892 30.5804C34.7051 30.3689 35.5664 30.6886 36.0467 31.3814C36.3372 31.7891 36.4518 32.2964 36.3648 32.7894C36.2775 33.2728 35.991 33.6973 35.5754 33.959L27.1734 39.3132C27.014 39.4147 26.8399 39.4912 26.6573 39.54C25.8417 39.7517 24.9807 39.4321 24.5007 38.7396C24.2437 38.3763 24.1237 37.9337 24.1618 37.4903L24.1906 37.1787L23.8783 37.084C22.6973 36.7276 21.5858 36.1723 20.5917 35.4419L20.1613 35.1257L20.0031 35.6082C19.9181 35.8648 19.851 36.1269 19.8023 36.3928C19.5115 38.0285 19.8919 39.7125 20.8577 41.0643Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,7 @@
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.275 15.2562L31.0001 7.98438L50.9026 19.3561V25.088L31.0001 13.7138L18.2999 20.9707L18.275 15.2562Z" fill="#7E7E7E"/>
<path d="M16.0733 39.2163L28.7611 46.4658L23.7979 49.3591L11.0977 42.1022V19.3561L16.0409 16.5325L16.0683 22.2544L16.0733 22.2569V39.2163Z" fill="#7E7E7E"/>
<path d="M45.927 24.8243V39.2163L31.0001 47.7445V47.752L26.0419 50.6428L31.0001 53.4764L50.9026 42.1022V27.6679L45.927 24.8243Z" fill="#7E7E7E"/>
<path d="M31.0002 16.2938L19.4393 22.8989L32.1296 30.1508L43.688 23.5432L31.0002 16.2938Z" fill="#7E7E7E"/>
<path d="M18.3124 24.8344V37.9153L31.0002 45.1672V32.0839L18.3124 24.8344Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 737 B

@@ -0,0 +1,3 @@
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.0532 13.4531H50.1147L30.2756 47.8158L10.4365 13.4531H17.4978L30.2755 35.5845L43.0532 13.4531ZM42.1764 13.4531L30.2755 34.0659L18.3746 13.4531L25.6939 13.4531L30.2756 21.3888L34.8572 13.4531L42.1764 13.4531Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 381 B

@@ -0,0 +1,3 @@
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8507 12.3604C27.1186 12.136 28.5596 12.0073 30.0686 12.0003C31.5775 11.9933 33.1508 12.108 34.6671 12.3604C37.0624 12.7596 39.0804 14.5565 39.0804 16.9486V25.3535C39.0804 27.8182 37.1213 29.8389 34.6671 29.8389H25.8507C22.8573 29.8389 20.3366 32.4087 20.3366 35.3221V39.3548H17.3017C14.7365 39.3548 13.2381 37.4929 12.6106 34.8798C11.7643 31.3692 11.8002 29.2712 12.6106 25.909C13.3132 22.9758 15.5595 21.434 18.1247 21.434H21.4373H30.264V20.3126H21.4373V16.9486C21.4373 14.4014 22.1155 13.0203 25.8507 12.3604ZM26.9514 16.3931C26.9514 15.4626 26.2069 14.7059 25.2952 14.7059C24.3801 14.7059 23.6389 15.4626 23.6389 16.3931C23.6389 17.3202 24.3801 18.0699 25.2952 18.0699C26.2069 18.0699 26.9514 17.3202 26.9514 16.3931ZM40.1814 25.3549V21.4353H43.494C46.0625 21.4353 47.2734 23.3566 47.9073 25.9104C48.7897 29.4573 48.8289 32.1162 47.9073 34.8811C47.0152 37.5668 46.0593 39.3562 43.494 39.3562H39.0807H30.2642V40.4775H39.0807V43.8415C39.0807 46.3887 36.8895 47.6835 34.6673 48.3269C31.3243 49.2969 28.6449 49.1485 25.8509 48.3269C23.5176 47.6406 21.4375 46.2336 21.4375 43.8415V35.4366C21.4375 33.0182 23.4359 30.9513 25.8509 30.9513H34.6673C37.6051 30.9513 40.1814 28.3936 40.1814 25.3549ZM36.8791 44.3971C36.8791 43.4699 36.1378 42.7202 35.2228 42.7202C34.3111 42.7202 33.5665 43.4699 33.5665 44.3971C33.5665 45.3275 34.3111 46.0842 35.2228 46.0842C36.1378 46.0842 36.8791 45.3275 36.8791 44.3971Z" fill="#7E7E7E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more