No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import { Input, Button } from 'ui'
|
||||
import Admonition from '~/components/Admonition'
|
||||
|
||||
function base64URL(value: string) {
|
||||
return globalThis.btoa(value).replace(/[=]/g, '').replace(/[+]/g, '-').replace(/[\/]/g, '_')
|
||||
}
|
||||
|
||||
/*
|
||||
Convert a string into an ArrayBuffer
|
||||
from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
*/
|
||||
function stringToArrayBuffer(value: string) {
|
||||
const buf = new ArrayBuffer(value.length)
|
||||
const bufView = new Uint8Array(buf)
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
bufView[i] = value.charCodeAt(i)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
function arrayBufferToString(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buf))
|
||||
}
|
||||
|
||||
const generateAppleSecretKey = async (
|
||||
kid: string,
|
||||
iss: string,
|
||||
sub: string,
|
||||
file: File
|
||||
): Promise<{ kid: string; jwt: string; exp: number }> => {
|
||||
if (!kid) {
|
||||
const match = file.name.match(/AuthKey_([^.]+)[.].*$/i)
|
||||
if (match && match[1]) {
|
||||
kid = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
if (!kid) {
|
||||
throw new Error(
|
||||
`No Key ID provided. The file "${file.name}" does not follow the AuthKey_XXXXXXXXXX.p8 pattern. Please provide a Key ID manually.`
|
||||
)
|
||||
}
|
||||
|
||||
const contents = await file.text()
|
||||
|
||||
if (!contents.match(/^\s*-+BEGIN PRIVATE KEY-+[^-]+-+END PRIVATE KEY-+\s*$/i)) {
|
||||
throw new Error(`Chosen file does not appear to be a PEM encoded PKCS8 private key file.`)
|
||||
}
|
||||
|
||||
// remove PEM headers and spaces
|
||||
const pkcs8 = stringToArrayBuffer(
|
||||
globalThis.atob(contents.replace(/-+[^-]+-+/g, '').replace(/\s+/g, ''))
|
||||
)
|
||||
|
||||
const privateKey = await globalThis.crypto.subtle.importKey(
|
||||
'pkcs8',
|
||||
pkcs8,
|
||||
{
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-256',
|
||||
},
|
||||
true,
|
||||
['sign']
|
||||
)
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000)
|
||||
const exp = iat + 180 * 24 * 60 * 60
|
||||
|
||||
const jwt = [
|
||||
base64URL(JSON.stringify({ typ: 'JWT', kid, alg: 'ES256' })),
|
||||
base64URL(
|
||||
JSON.stringify({
|
||||
iss,
|
||||
sub,
|
||||
iat,
|
||||
exp,
|
||||
aud: 'https://appleid.apple.com',
|
||||
})
|
||||
),
|
||||
]
|
||||
|
||||
const signature = await globalThis.crypto.subtle.sign(
|
||||
{
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
privateKey,
|
||||
stringToArrayBuffer(jwt.join('.'))
|
||||
)
|
||||
|
||||
jwt.push(base64URL(arrayBufferToString(signature)))
|
||||
|
||||
return { kid, jwt: jwt.join('.'), exp }
|
||||
}
|
||||
|
||||
const AppleSecretGenerator = () => {
|
||||
const [file, setFile] = useState({ file: null as File | null })
|
||||
const [teamID, setTeamID] = useState('')
|
||||
const [serviceID, setServiceID] = useState('')
|
||||
const [keyID, setKeyID] = useState('')
|
||||
const [secretKey, setSecretKey] = useState('')
|
||||
const [expiresAt, setExpiresAt] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
label="Account ID"
|
||||
labelOptional="required"
|
||||
placeholder="Apple Developer account ID, 10 alphanumeric digits"
|
||||
descriptionText="Found in the upper-right corner of Apple Developer Center."
|
||||
value={teamID}
|
||||
onChange={(e) => setTeamID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Service ID"
|
||||
labelOptional="required"
|
||||
placeholder="ID of the service, example: com.example.app.service"
|
||||
descriptionText="Found under Certificates, Identifiers & Profiles in Apple Developer Center."
|
||||
value={serviceID}
|
||||
onChange={(e) => setServiceID(e.target.value.trim())}
|
||||
/>
|
||||
<Input
|
||||
label="Key ID"
|
||||
labelOptional="(optional)"
|
||||
placeholder="Extracted from filename, AuthKey_XXXXXXXXXX.p8"
|
||||
descriptionText="If the file you select does not preserve the original name from Apple Developer Center, please enter the key ID."
|
||||
value={keyID}
|
||||
onChange={(e) => setKeyID(e.target.value.trim())}
|
||||
/>
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
setFile({ file: e.target.files[0] })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: '1rem' }} />
|
||||
|
||||
<Button
|
||||
size="medium"
|
||||
disabled={
|
||||
!(
|
||||
teamID.length === 10 &&
|
||||
serviceID &&
|
||||
((globalThis && globalThis.showOpenFilePicker) || file.file)
|
||||
)
|
||||
}
|
||||
onClick={async () => {
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const { kid, jwt, exp } = await generateAppleSecretKey(
|
||||
keyID,
|
||||
teamID,
|
||||
serviceID,
|
||||
file.file
|
||||
)
|
||||
setKeyID(kid)
|
||||
setSecretKey(jwt)
|
||||
setExpiresAt(new Date(exp * 1000).toString())
|
||||
setError('')
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
console.error(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Generate Secret Key
|
||||
</Button>
|
||||
|
||||
{error && <Admonition type="danger">{error}</Admonition>}
|
||||
|
||||
{secretKey && (
|
||||
<>
|
||||
<div style={{ height: '1rem' }} />
|
||||
<Input
|
||||
label="Secret Key"
|
||||
value={secretKey}
|
||||
descriptionText={`Valid until: ${expiresAt}. Make sure you generate a new one before then!`}
|
||||
reveal
|
||||
copy
|
||||
size="medium"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppleSecretGenerator
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Tabs } from 'ui'
|
||||
export const TabPanel = Tabs.Panel
|
||||
|
||||
## Project setup
|
||||
|
||||
Let's create a new Postgres database. This is as simple as starting a new Project in Supabase:
|
||||
|
||||
1. [Create a new project](https://database.new/) in the Supabase dashboard.
|
||||
1. Enter your project details. Remember to store your password somewhere safe.
|
||||
|
||||
Your database will be available in less than a minute.
|
||||
|
||||
**Finding your credentials:**
|
||||
|
||||
You can find your project credentials inside the project [settings](https://app.supabase.com/project/_/settings/), including:
|
||||
|
||||
- [Database credentials](https://app.supabase.com/project/_/settings/database): connection strings and connection pooler details.
|
||||
- [API credentials](https://app.supabase.com/project/_/settings/database): your serverless API URL and `anon` / `service_role` keys.
|
||||
@@ -373,10 +373,11 @@ export function IconMenuAI({ width = 16, height = 16 }: HomeMenuIcon) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.59777 3.20772C7.25701 4.22996 6.68287 5.15901 5.92094 5.92094C5.15901 6.68288 4.23014 7.25695 3.2079 7.59771L2 8L3.20772 8.40223C4.22996 8.74299 5.15901 9.31712 5.92094 10.0791C6.68287 10.841 7.25695 11.7699 7.59771 12.7921L8 14L8.40223 12.7923C8.74299 11.77 9.31713 10.841 10.0791 10.0791C10.841 9.31712 11.7699 8.74305 12.7921 8.40229L14 8L12.7923 7.59777C11.77 7.25701 10.841 6.68288 10.0791 5.92094C9.31713 5.15901 8.74305 4.23014 8.40229 3.2079L8 2L7.59777 3.20772Z"
|
||||
d="M7.99886 7.63216V14.4892M7.99886 7.63216L14.0488 4.11804M7.99886 7.63216L1.94922 4.11819M1.94922 4.11819V8.32332M1.94922 4.11819V4.08217L5.57319 1.97717M14.049 8.36007V4.08217L10.4251 1.97717M11.8165 12.4072L7.99913 14.6245L4.18177 12.4072"
|
||||
stroke="currentColor"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="bevel"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -508,14 +508,33 @@ export const database: NavMenuConstant = {
|
||||
title: 'Database',
|
||||
url: '/guides/database',
|
||||
items: [
|
||||
{ name: 'Database Connections', url: '/guides/database/connecting-to-postgres' },
|
||||
{ name: 'Tables and Data', url: '/guides/database/tables' },
|
||||
{ name: 'Database Functions', url: '/guides/database/functions' },
|
||||
{ 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: 'Overview', url: '/guides/database' },
|
||||
{
|
||||
name: 'Fundamentals',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres' },
|
||||
{ name: 'Managing tables, views, and data', url: '/guides/database/tables' },
|
||||
{ name: 'Managing database functions', url: '/guides/database/functions' },
|
||||
{ name: 'Managing indexes', url: '/guides/database/postgres/indexes' },
|
||||
{ name: 'Managing database webhooks', url: '/guides/database/webhooks' },
|
||||
{ name: 'Managing database replication', url: '/guides/database/replication' },
|
||||
{ name: 'Managing secrets with Vault', url: '/guides/database/vault' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Postgres Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Implementing Full Text Search', url: '/guides/database/full-text-search' },
|
||||
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
|
||||
{ name: 'Implementing column encryption', url: '/guides/database/column-encryption' },
|
||||
{ name: 'Testing your database', url: '/guides/database/testing' },
|
||||
{ name: 'Managing Timeouts', url: '/guides/database/timeouts' },
|
||||
{ name: 'Managing Passwords', url: '/guides/database/managing-passwords' },
|
||||
{ name: 'Configuring Timezones', url: '/guides/database/managing-timezones' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Extensions',
|
||||
url: undefined,
|
||||
@@ -622,17 +641,9 @@ export const database: NavMenuConstant = {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Postgres resources',
|
||||
name: 'Examples',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Managing Indexes',
|
||||
url: '/guides/database/postgres/indexes',
|
||||
},
|
||||
{
|
||||
name: 'Cascade Deletes',
|
||||
url: '/guides/database/postgres/cascade-deletes',
|
||||
},
|
||||
{
|
||||
name: 'Drop All Tables in Schema',
|
||||
url: '/guides/database/postgres/dropping-all-tables-in-schema',
|
||||
@@ -647,16 +658,6 @@ export const database: NavMenuConstant = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Timeouts', url: '/guides/database/timeouts' },
|
||||
{ name: 'Replication', url: '/guides/database/replication' },
|
||||
{ name: 'Passwords', url: '/guides/database/managing-passwords' },
|
||||
{ name: 'Timezones', url: '/guides/database/managing-timezones' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -726,7 +727,7 @@ export const functions: NavMenuConstant = {
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Developing Functions locally', url: '/guides/functions/local-development' },
|
||||
{ name: 'Deploying with Git', url: '/guides/functions/cicd-workflow' },
|
||||
{ name: 'Deploying with GitHub', url: '/guides/functions/cicd-workflow' },
|
||||
{ name: 'Managing Secrets and Environment Variables', url: '/guides/functions/secrets' },
|
||||
{ name: 'Integrating With Supabase Auth', url: '/guides/functions/auth' },
|
||||
{
|
||||
@@ -862,13 +863,18 @@ export const ai: NavMenuConstant = {
|
||||
{ name: 'Overview', url: '/guides/ai' },
|
||||
{ name: 'Concepts', url: '/guides/ai/concepts' },
|
||||
{
|
||||
name: 'Structured & unstructured embeddings',
|
||||
url: '/guides/ai/structured-unstructured-embeddings',
|
||||
name: 'Structured & unstructured',
|
||||
url: '/guides/ai/structured-unstructured',
|
||||
},
|
||||
{
|
||||
name: 'Quickstarts',
|
||||
url: undefined,
|
||||
items: [{ name: 'Python client', url: '/guides/ai/vecs-python-client' }],
|
||||
items: [
|
||||
{ name: 'Developing locally with Vecs', url: '/guides/ai/vecs-python-client' },
|
||||
{ name: 'Creating and managing collections', url: '/guides/ai/quickstarts/hello-world' },
|
||||
{ name: 'Text Deduplication', url: '/guides/ai/quickstarts/text-deduplication' },
|
||||
{ name: 'Face similarity search', url: '/guides/ai/quickstarts/face-similarity' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Guides',
|
||||
|
||||
@@ -16,6 +16,7 @@ import FunctionsExamples from './FunctionsExamples'
|
||||
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
|
||||
import RefSubLayout from '~/layouts/ref/RefSubLayout'
|
||||
import { Heading } from './CustomHTMLElements'
|
||||
import DatabaseSetup from './MDX/database_setup.mdx'
|
||||
import ProjectSetup from './MDX/project_setup.mdx'
|
||||
import QuickstartIntro from './MDX/quickstart_intro.mdx'
|
||||
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
|
||||
@@ -66,6 +67,7 @@ const components = {
|
||||
FunctionsExamples,
|
||||
JwtGenerator,
|
||||
QuickstartIntro,
|
||||
DatabaseSetup,
|
||||
ProjectSetup,
|
||||
SocialProviderSetup,
|
||||
SocialProviderSettingsSupabase,
|
||||
|
||||
@@ -53,14 +53,14 @@ Compared to our 2-dimensional example above, most embedding models will output m
|
||||
Why is this useful? Once we have generated embeddings on multiple texts, it is trivial to calculate how similar they are using vector math operations like cosine distance. A common use case for this is search. Your process might look something like this:
|
||||
|
||||
1. Pre-process your knowledge base and generate embeddings for each page
|
||||
2. Store your embeddings to be referenced later (more on this)
|
||||
2. Store your embeddings to be referenced later
|
||||
3. Build a search page that prompts your user for input
|
||||
4. Take user's input, generate a one-time embedding, then perform a similarity search against your pre-processed embeddings.
|
||||
5. Return the most similar pages to the user
|
||||
|
||||
## See also
|
||||
|
||||
- [Structured and Unstructured embeddings](/docs/guides/ai/structured-unstructured-embeddings)
|
||||
- [Structured and Unstructured embeddings](/docs/guides/ai/structured-unstructured)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -16,7 +16,18 @@ For small workloads it's typical to store your data in a single database.
|
||||
|
||||
If you've used [Vecs](/docs/guides/ai/vecs-python-client) to create 3 different collections, you can expose collections to your web or mobile application using [views](/docs/guides/database/tables#views):
|
||||
|
||||

|
||||
<div>
|
||||
<img
|
||||
alt="single database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--single-database--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="single database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
For example, with 3 collections, called `docs`, `posts`, and `images`, we could expose the "docs" inside the public schema like this:
|
||||
|
||||
@@ -44,7 +55,18 @@ const { data, error } = await supabase
|
||||
|
||||
As you move into production, we recommend running splitting your collections into separate projects. This is because it allows your vector stores to scale independently of your production data. Vectors typically grow faster than operational data, and they have different resource requirements. Running them on separate databases removes the single-point-of-failure.
|
||||
|
||||

|
||||
<div>
|
||||
<img
|
||||
alt="With secondaries"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="With secondaries"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
You can use as many secondary databases as you need to manage your collections. With this architecture, you have 2 options for accessing collections within your application:
|
||||
|
||||
@@ -120,7 +142,18 @@ const { data, error } = await supabase
|
||||
|
||||
This diagram provides an example architecture, allowing you to access the collections either with our client libraries or using Vecs. You can add as many secondary databases as you need, in this example we show one only:
|
||||
|
||||

|
||||
<div>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="dark:hidden"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png"
|
||||
/>
|
||||
<img
|
||||
alt="multi database"
|
||||
className="hidden dark:block"
|
||||
src="/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -8,7 +8,14 @@ export const meta = {
|
||||
sidebar_label: 'Google Colab',
|
||||
}
|
||||
|
||||
Google Colab is a hosted Jupyter Notebook service. It provides free access to computing resources, including GPUs and TPUs, and is well-suited to machine learning, data science, and education. We can use Colab to manage collections using [Supabase Vecs](/docs/ai/vecs-python-client).
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
Google Colab is a hosted Jupyter Notebook service. It provides free access to computing resources, including GPUs and TPUs, and is well-suited to machine learning, data science, and education. We can use Colab to manage collections using [Supabase Vecs](/docs/guides/ai/vecs-python-client).
|
||||
|
||||
In this tutorial we'll connect to a database running on the Supabase [platform](https://app.supabase.com/). If you don't already have a database, you can create one here: [database.new](https://database.new).
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Face similarity search',
|
||||
subtitle: 'Identify the celebrities you looks most similar to using Supabase Vecs.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a ["Face Similarity Search"](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) example using Colab and Supabase Vecs. You'll identify the celebrities you (or any other person) looks most similar to. You will:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Load the "`ashraq/tmdb-people-image`" celebrity dataset
|
||||
1. Use the `face_recognition` model to create an embedding for every celebrity photo.
|
||||
1. Search for similar faces inside the dataset.
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,63 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Creating and managing collections',
|
||||
subtitle: 'Connecting to your database with Colab.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a basic ["Hello World"](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) example using Colab and Supabase Vecs. You'll learn how to:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Create a vector collection
|
||||
1. Add data to the collection
|
||||
1. Query the collection
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`vector_hello_world`](https://github.com/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,63 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'ai-vecs-python-client',
|
||||
title: 'Semantic Text Deduplication',
|
||||
subtitle: 'Finding duplicate movie reviews with Supabase Vecs.',
|
||||
breadcrumb: 'AI Quickstarts',
|
||||
}
|
||||
|
||||
This guide will walk you through a ["Semantic Text Deduplication"](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) example using Colab and Supabase Vecs. You'll learn how to find similar movie reviews using embeddings, and remove any that seem like duplicates. You will:
|
||||
|
||||
1. Launch a Postgres database that uses pgvector to store embeddings
|
||||
1. Launch a notebook that connects to your database
|
||||
1. Load the IMDB dataset
|
||||
1. Use the `sentence-transformers/all-MiniLM-L6-v2` model to create an embedding representing the semantic meaning of each review.
|
||||
1. Search for all duplicates.
|
||||
|
||||
<DatabaseSetup />
|
||||
|
||||
## Launching a notebook
|
||||
|
||||
Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb) notebook in Colab:
|
||||
|
||||
<a
|
||||
className="w-64"
|
||||
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb"
|
||||
>
|
||||
<img src="/docs/img/ai/colab-badge.svg" />
|
||||
</a>
|
||||
|
||||
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
|
||||
|
||||
## Connecting to your database
|
||||
|
||||
Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:
|
||||
|
||||
```python
|
||||
import vecs
|
||||
|
||||
DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"
|
||||
|
||||
# create vector store client
|
||||
vx = vecs.create_client(DB_CONNECTION)
|
||||
```
|
||||
|
||||
Replace the `DB_CONNECTION` with your own connection string for your database, which you set up in first step of this guide.
|
||||
|
||||
## Stepping through the notebook
|
||||
|
||||
Now all that's left is to step through the notebook. You can do this by clicking the "execute" button (`ctrl+enter`) at the top left of each code cell. The notebook guides you through the process of creating a collection, adding data to it, and querying it.
|
||||
|
||||
You can view the inserted items in the [Table Editor](https://app.supabase.com/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.
|
||||
|
||||

|
||||
|
||||
## Next steps
|
||||
|
||||
You can now start building your own applications with Vecs. Check our [examples](/docs/guides/ai#examples) for ideas.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -1,61 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'structured-unstructured-embeddings',
|
||||
title: 'Structured and unstructured embeddings',
|
||||
description:
|
||||
'Supabase is flexible enough to provide structured and unstructured embeddings using pgvector.',
|
||||
subtitle:
|
||||
'Supabase is flexible enough to provide structured and unstructured embeddings using pgvector.',
|
||||
sidebar_label: 'Structured and unstructured embeddings',
|
||||
}
|
||||
|
||||
Most vector stores treat embeddings like NoSQL, unstructured data. Supabase is flexible enough to fit either a structured or an unstructured approach.
|
||||
|
||||
Compare these code snippets:
|
||||
|
||||
## Structured
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
content text,
|
||||
url string,
|
||||
embedding vector(1536)
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, content, url, embedding)
|
||||
values
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', 'Hello world', '/hello-world', [100, 200, 300]);
|
||||
```
|
||||
|
||||
A structured approach is usually defined in SQL, and managed via database [migrations](/docs/guides/getting-started/local-development#database-migrations).
|
||||
|
||||
## Unstructured
|
||||
|
||||
```py
|
||||
import vecs
|
||||
|
||||
docs = vx.create_collection(name="docs", dimension=1536)
|
||||
|
||||
docs.upsert(vectors=[
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', [100, 200, 300], { url: '/hello-world' })
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
An unstructured approach is usually defined in Python and has a looser table definition, storing metadata as a json document along with the embedding.
|
||||
|
||||
## Choosing the right model
|
||||
|
||||
Both approaches create a table where you can store your embeddings and some metadata. You should choose the best approach for your use-case.
|
||||
|
||||
- Structured embeddings are typically co-located with some content that is already stored in your database.
|
||||
- Unstructured embeddings are typically defined at runtime, better-suited for a large body of external content.
|
||||
|
||||
Both approaches are fine, and the one you should choose depends on your use-case.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,114 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'structured-unstructured-embeddings',
|
||||
title: 'Structured and Unstructured',
|
||||
description:
|
||||
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
|
||||
subtitle:
|
||||
'Supabase is flexible enough to associate structured and unstructured metadata with embeddings.',
|
||||
sidebar_label: 'Structured and unstructured embeddings',
|
||||
}
|
||||
|
||||
Most vector stores treat metadata associated with embeddings like NoSQL, unstructured data. Supabase is flexible enough to store unstructured and structured metadata.
|
||||
|
||||
## Structured
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
content text,
|
||||
url string
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, content, url, embedding)
|
||||
values
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', array[0.1, 0.2, 0.3], 'Hello world', '/hello-world');
|
||||
```
|
||||
|
||||
Notice that we've associated two pieces of metadata, `content` and `url`, with the embedding. Those fields can be filtered, constrained, indexed, and generally operated on using the full power of SQL. Structured metadata fits naturally with a traditional Supabase application, and can be managed via database [migrations](/docs/guides/getting-started/local-development#database-migrations).
|
||||
|
||||
## Unstructured
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
meta jsonb
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, embedding, meta)
|
||||
values
|
||||
(
|
||||
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
|
||||
array[0.1, 0.2, 0.3],
|
||||
'{"content": "Hello world", "url": "/hello-world"}'
|
||||
);
|
||||
```
|
||||
|
||||
An unstructured approach does not specify the metadata fields that are expected. It stores all metadata in a flexible `json`/`jsonb` column. The tradeoff is that the querying/filtering capabilities of a schemaless data type are less flexible than when each field has a dedicated column. It also pushes the burden of metadata data integrity onto application code, which is more error prone than enforcing constraints in the database.
|
||||
|
||||
The unstructured approach is recommended:
|
||||
|
||||
- for ephemeral/interactive workloads e.g. data science or scientific research
|
||||
- when metadata fields are user-defined or unknown
|
||||
- during rapid prototyping
|
||||
|
||||
Client libraries like python's [vecs](https://github.com/supabase/vecs) use this structure. For example, running:
|
||||
|
||||
```py
|
||||
#!/usr/bin/env python3
|
||||
import vecs
|
||||
|
||||
docs = vx.create_collection(name="docs", dimension=1536)
|
||||
|
||||
docs.upsert(vectors=[
|
||||
('79409372-7556-4ccc-ab8f-5786a6cfa4f7', [100, 200, 300], { url: '/hello-world' })
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
automatically creates the unstructured SQL table during the call to `create_collection`.
|
||||
|
||||
Note that when working with client libraries that emit SQL DDL, like `create table ...`, you should add that SQL to your migrations when moving to production to maintain a single source of truth for your database's schema.
|
||||
|
||||
## Hybrid
|
||||
|
||||
The structured metadata style is recommended when the fields being tracked are known in advance. If you have a combination of known and unknown metadata fields, you can accommodate the unknown fields by adding a `json`/`jsonb` column to the table. In that situation, known fields should continue to use dedicated columns for best query performance and throughput.
|
||||
|
||||
```sql
|
||||
create table docs (
|
||||
id uuid primary key,
|
||||
embedding vector(3),
|
||||
content text,
|
||||
url string,
|
||||
meta jsonb
|
||||
);
|
||||
|
||||
insert into docs
|
||||
(id, embedding, meta)
|
||||
values
|
||||
(
|
||||
'79409372-7556-4ccc-ab8f-5786a6cfa4f7',
|
||||
array[0.1, 0.2, 0.3],
|
||||
'Hello world',
|
||||
'/hello-world',
|
||||
'{"key": "value"}'
|
||||
);
|
||||
```
|
||||
|
||||
## Choosing the right model
|
||||
|
||||
Both approaches create a table where you can store your embeddings and some metadata. You should choose the best approach for your use-case. In summary:
|
||||
|
||||
- Structured metadata is best when fields are known in advance or query patterns are predictable e.g. a production Supabase application
|
||||
- Unstructured metadata is best when fields are unknown/user-defined or when working with data interactively e.g. exploratory research
|
||||
|
||||
Both approaches are valid, and the one you should choose depends on your use-case.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -74,6 +74,17 @@ Supabase provides a Realtime API using [Realtime](https://github.com/supabase/re
|
||||
Realtime leverages PostgreSQL's built-in logical replication. You can manage your Realtime API simply by managing Postgres publications.
|
||||
Go to your project's [Replication section](https://app.supabase.com/project/_/database/replication) to get started.
|
||||
|
||||
## API URL and Keys
|
||||
|
||||
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -95,7 +95,7 @@ Future<void> signOut() async {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
|
||||
@@ -79,6 +79,22 @@ const App = () => (
|
||||
)
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
Options are available via 'queryParams':
|
||||
|
||||
```<Auth
|
||||
supabaseClient={supabase}
|
||||
providers={['google']}
|
||||
queryParams={{
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
hd: 'domain.com'
|
||||
}}
|
||||
onlyThirdPartyProviders={true}
|
||||
/>
|
||||
```
|
||||
|
||||
### Supported Views
|
||||
|
||||
The Auth component is currently shipped with the following views:
|
||||
|
||||
@@ -521,6 +521,14 @@ export default function Home() {
|
||||
|
||||
> check out [this repo](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs) for more examples, including [realtime subscriptions](https://github.com/supabase/supabase/tree/master/examples/auth/nextjs/app/realtime-posts.tsx).
|
||||
|
||||
#### Singleton
|
||||
|
||||
The `createClientComponentClient` function implements a [Singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) to simplify instantiating Supabase clients. If you need multiple Supabase instances across Client Components - for example, when using multiple schemas - you can pass an additional configuration option for `{ isSingleton: false }` to get a new client every time this function is called.
|
||||
|
||||
```jsx
|
||||
const supabase = createClientComponentClient({ isSingleton: false })
|
||||
```
|
||||
|
||||
### Server Component
|
||||
|
||||
[Server Components](https://nextjs.org/docs/getting-started/react-essentials#server-components) allow for asynchronous data to be fetched server-side.
|
||||
@@ -736,7 +744,7 @@ With v0.7.x of the Next.js Auth Helpers a new naming convention has been impleme
|
||||
|
||||
#### createClientComponentClient returns singleton
|
||||
|
||||
You no longer need to implement logic to ensure there is only a single instance of the Supabase Client shared across all Client Components - this is now handled by the `createClientComponentClient` function. Call it as many times as you want!
|
||||
You no longer need to implement logic to ensure there is only a single instance of the Supabase Client shared across all Client Components - this is now the default and handled by the `createClientComponentClient` function. Call it as many times as you want!
|
||||
|
||||
```jsx
|
||||
"use client";
|
||||
@@ -749,6 +757,8 @@ export default function() {
|
||||
}
|
||||
```
|
||||
|
||||
For an example of creating multiple Supabase clients, check [Singleton section](/docs/guides/auth/auth-helpers/nextjs#singleton) above.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -88,7 +88,7 @@ Future<void> signOut() async {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ We are using `next` as our query parameter, but this can name whatever you like.
|
||||
The email link you receive will behave like a magic link. When the link is clicked you will be sent to the `redirectTo` URL you specified that points to the path with the exchange code.
|
||||
|
||||
### Exchange authorization code
|
||||
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeAuthCodeForSession` function.
|
||||
After redirecting to the server page, we need to retrieve the code from the query parameter called `code` and pass it to the `.exchangeCodeForSession` function.
|
||||
```ts
|
||||
// api/auth/callback.ts
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import AppleSecretGenerator from '~/components/AppleSecretGenerator'
|
||||
|
||||
export const meta = {
|
||||
id: 'auth-apple',
|
||||
@@ -89,69 +90,15 @@ Now you'll need to download a `secret key` file from Apple that will be used to
|
||||
- Save the downloaded file -- this contains your "secret key" that will be used to generate your `client_secret`.
|
||||
- Click `Done` at the top right.
|
||||
|
||||
## Generate a `client_secret`
|
||||
## Generate a client secret
|
||||
|
||||
The `secret key` you downloaded is used to create the `client_secret` string you'll need to authenticate your users.
|
||||
You need to configure a client secret when using Sign in with Apple for Web. This is a specially crafted [JWT signed with a secret key downloaded from Apple's Developer Center](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens).
|
||||
|
||||
According to the [Apple Docs](https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens) it needs to be a JWT
|
||||
token encrypted using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the P-256 curve and the SHA-256 hash algorithm.
|
||||
<Admonition>
|
||||
Use this tool to generate a new Apple client secret. No keys leave your browser!
|
||||
</Admonition>
|
||||
|
||||
At this time, the easiest way to generate this JWT token is with [Ruby](https://www.ruby-lang.org/en/).
|
||||
If you don't have Ruby installed, you can [Download Ruby Here](https://www.ruby-lang.org/en/downloads).
|
||||
|
||||
- Install Ruby (or check to make sure it's installed on your system).
|
||||
- Install [ruby-jwt](https://github.com/jwt/ruby-jwt).
|
||||
- From the command line, run: `sudo gem install jwt`.
|
||||
|
||||
Create the script below using a text editor: `secret_gen.rb`
|
||||
|
||||
```ruby
|
||||
require "jwt"
|
||||
|
||||
key_file = "Path to the private key"
|
||||
team_id = "Your Team ID"
|
||||
client_id = "The Service ID of the service you created"
|
||||
key_id = "The Key ID of the private key"
|
||||
|
||||
validity_period = 180 # In days. Max 180 (6 months) according to Apple docs.
|
||||
|
||||
private_key = OpenSSL::PKey::EC.new IO.read key_file
|
||||
|
||||
token = JWT.encode(
|
||||
{
|
||||
iss: team_id,
|
||||
iat: Time.now.to_i,
|
||||
exp: Time.now.to_i + 86400 * validity_period,
|
||||
aud: "https://appleid.apple.com",
|
||||
sub: client_id
|
||||
},
|
||||
private_key,
|
||||
"ES256",
|
||||
header_fields=
|
||||
{
|
||||
kid: key_id
|
||||
}
|
||||
)
|
||||
puts token
|
||||
```
|
||||
|
||||
1. Edit the `secret_gen.rb` file:
|
||||
|
||||
- `key_file` = "Path to the private key you downloaded from Apple". It should look like this: `AuthKey_XXXXXXXXXX.p8`.
|
||||
- `team_id` = "Your Team ID". This is found at the Apple Developer website, under Membership details. This is a 10-character alphanumeric string called "Team ID". Alternatively, this can be seen next to your name in the upper right when viewing your Certificates, Identifiers & Profiles.
|
||||
- `client_id` = "The Service ID of the service you created". This is the `Services ID` you created in the above step `Obtain a Services ID`. If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Identifiers` at the left.
|
||||
- At the top right drop-down, select `Services IDs`.
|
||||
- Find your Identifier in the list (i.e. app.com.acme.roadrunner).
|
||||
- `key_id` = "The Key ID of the private key". This can be found in the name of your downloaded secret file (For a file named `AuthKey_XXXXXXXXXX.p8` your key_id is `XXXXXXXXXX`). If you've lost this ID, you can find it in the Apple Developer Site:
|
||||
- Go to `Certificates, Identifiers & Profiles`.
|
||||
- Click `Keys` at the left.
|
||||
- Click on your newly-created key in the list.
|
||||
- Look under `Key ID` to find your key_id.
|
||||
|
||||
2. From the command line, run: `ruby secret_gen.rb > client_secret.txt`.
|
||||
3. Your `client_secret` is now stored in this `client_secret.txt` file.
|
||||
<AppleSecretGenerator />
|
||||
|
||||
## Add your OAuth credentials to Supabase
|
||||
|
||||
@@ -180,8 +127,6 @@ async function signout() {
|
||||
## Resources
|
||||
|
||||
- [Apple Developer Account](https://developer.apple.com).
|
||||
- [Ruby](https://www.ruby-lang.org/en/) Docs.
|
||||
- [ruby-jwt](https://github.com/jwt/ruby-jwt) library.
|
||||
- Thanks to [Janak Amarasena](https://medium.com/@janakda) who did all the heavy lifting in [How to configure Sign In with Apple](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003).
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -68,7 +68,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Bitbucket Account](https://bitbucket.org)
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Discord Account](https://discord.com)
|
||||
- [Discord Developer Portal](https://discord.com/developers)
|
||||
|
||||
@@ -83,7 +83,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Facebook Developers Dashboard](https://developers.facebook.com/)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitLab Account](https://gitlab.com)
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ async function signInWithGoogle() {
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent',
|
||||
hd: 'domain.com //google will also allowo OAuth logins to be restricted to a specified domain using the 'hd' parameter
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -113,7 +114,7 @@ async function signInWithGoogle() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Google Cloud Platform Console](https://console.cloud.google.com/home/dashboard)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Notion Account](https://notion.so)
|
||||
- [Notion Developer Portal](https://www.notion.so/my-integrations)
|
||||
|
||||
@@ -80,7 +80,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Slack Developer Dashboard](https://api.slack.com/apps)
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitch Account](https://twitch.tv)
|
||||
- [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
|
||||
@@ -73,7 +73,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard)
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ async function signout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Zoom App Marketplace](https://marketplace.zoom.us/)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ You can use the `supabase sso` [subcommands](/docs/reference/cli/supabase-sso) t
|
||||
|
||||
SAML 2.0 support is disabled by default on Supabase projects. You can configure this on the [Auth Providers](https://app.supabase.com/project/_/auth/providers) page on your project.
|
||||
|
||||
Please note that SAML 2.0 support is offered on tiers Pro and above. Check the [Pricing](https://supabase.com/pricing) page for more information.
|
||||
Please note that SAML 2.0 support is offered on plans Pro and above. Check the [Pricing](https://supabase.com/pricing) page for more information.
|
||||
|
||||
## Terminology
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ returns:
|
||||
## Resources
|
||||
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -3,20 +3,17 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'column-encryption',
|
||||
title: 'Column Encryption',
|
||||
description: 'Use Supabase to store and serve files.',
|
||||
description: 'Encrypt columns using Transparent Column Encryption.',
|
||||
subtitle: 'Encrypt columns using Transparent Column Encryption.',
|
||||
sidebar_label: 'Overview',
|
||||
video: 'https://www.youtube.com/v/J9mTPY8rIXE',
|
||||
}
|
||||
|
||||
Encrypted Columns for Tables
|
||||
Supabase provides a secure method for encrypting columns using [Vault](/docs/guides/database/vault), our Postgres secrets manager. Vault is Postgres extension with an integrated UI intended to act as a secure global secrets management for you project.
|
||||
|
||||
## Transparent Column Encryption (TCE)
|
||||
Vault enables an advanced feature called Transparent Column Encryption (TCE) which 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 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
|
||||
## 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.
|
||||
|
||||
@@ -26,10 +23,50 @@ Once you've created an encrypted column, you can insert data into the table like
|
||||
|
||||

|
||||
|
||||
## Decrypting data
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## 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**.
|
||||
|
||||
## How Key Derivation Works
|
||||
|
||||
The current state-of-the-art in encryption libraries is [libsodium](https://doc.libsodium.org/).
|
||||
@@ -202,43 +239,7 @@ security label for pgsodium
|
||||
|
||||
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**.
|
||||
## Resources
|
||||
|
||||
- [Supabase Vault](/docs/guides/database/vault)
|
||||
- Read more about Supabase Vault in the [blog post](https://supabase.com/blog/vault-now-in-beta)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
|
||||
export const meta = {
|
||||
id: 'connecting-to-postgres',
|
||||
@@ -8,54 +9,21 @@ export const meta = {
|
||||
|
||||
Supabase provides several options for programmatically connecting to your Postgres database:
|
||||
|
||||
## Types of Connection
|
||||
1. Direct connections using Postgres' standard connection system
|
||||
2. Connection pooling using PgBouncer
|
||||
3. Programmatic access uing the [Serverless APIs](/docs/guides/api)
|
||||
|
||||
- HTTP connections using the API.
|
||||
- Direct connections using Postgres' standard connection system.
|
||||
- Connection pooling using PgBouncer.
|
||||
## Serverless APIs
|
||||
|
||||
### Direct vs Pooling vs API
|
||||
|
||||
- A "direct connection" is when a connection is made to the database using Postgres' native connection implementation. You should use this for tools which are always alive - usually installed on a long-running server.
|
||||
- A "connection pool" is a system (external to Postgres) which keeps connections "open". You should use this for serverless functions and tools which disconnect from the database frequently.
|
||||
- The API is an auto-generated REST interface. You should use this for all browser and application interactions. The API server internally handles a connection pool.
|
||||
|
||||
Why would you use a connection pool? Primarily because the way that Postgres handles connections isn't very scalable for a large number of _temporary_ connections.
|
||||
You can use these simple questions to determine which connection method to use:
|
||||
|
||||
- Are you connecting to a database and _maintaining_ a connection? If yes, use a direct connection.
|
||||
- Are you connecting to your database and then _disconnecting_ immediately (e.g. a serverless environment)? If yes, use a connection pool.
|
||||
|
||||
## API
|
||||
|
||||
Supabase provides an auto-updating [API](/docs/guides/database/api). This is the easiest way to get started if you are managing data (fetching, inserting, updating).
|
||||
|
||||
### Interfaces
|
||||
|
||||
We provides several types of API to suit your preferences and use-case:
|
||||
Supabase provides auto-updating [APIs](/docs/guides/database/api). This is the easiest way to get started if you are managing data (fetching, inserting, updating). We provides several types of API to suit your preferences:
|
||||
|
||||
- [REST](/docs/guides/database/api#rest-api): interact with your database through a REST interface.
|
||||
- [GraphQL](/docs/guides/database/api#graphql-api): interact with your database through a GraphQL interface.
|
||||
- [Realtime](/docs/guides/database/api#realtime-api): listen to database changes over websockets.
|
||||
|
||||
You cannot manage the database schema via the API (for security reasons). To do that you can use the dashboard or connect directly to your database.
|
||||
|
||||
### API URL and Keys
|
||||
|
||||
You can find the API URL and Keys in the [Dashboard](https://app.supabase.com/project/_/settings/api).
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Direct connections
|
||||
|
||||
Every Supabase project provides a full Postgres database. You can connect to the database using any tool which supports Postgres.
|
||||
|
||||
### Finding your connection string
|
||||
Every Supabase project provides a full Postgres database. You can connect to the database using [any tool which supports Postgres](#integrations). You can find the connection string in the [Database settings](https://app.supabase.com/project/_/settings/database) inside the dashboard:
|
||||
|
||||
1. Go to the `Settings` section.
|
||||
2. Click `Database`.
|
||||
@@ -68,42 +36,9 @@ Every Supabase project provides a full Postgres database. You can connect to the
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Connection Pool
|
||||
## Connection Pooler
|
||||
|
||||
Connection pools are useful for managing a large number of _temporary_ connections. For example, if you are using [Prisma](/docs/guides/integrations/prisma) deployed to a Serverless environment.
|
||||
|
||||
### How connection pooling works
|
||||
|
||||
A "connection pool" is a system (external to Postgres) which manages connections, rather than PostgreSQL's native system. Supabase uses [PgBouncer](https://www.pgbouncer.org/) for connection pooling.
|
||||
|
||||
When a client makes a request, PgBouncer "allocates" an available connection to the client.
|
||||
When the client transaction or session is completed the connection is returned to the pool and is free to be used by another client.
|
||||
|
||||

|
||||
|
||||
### Pool modes
|
||||
|
||||
Pool Mode determines how PgBouncer handles a connection.
|
||||
|
||||
#### Session
|
||||
|
||||
When a new client connects, a connection is assigned to the client until it disconnects. Afterward, the connection is returned back to the pool.
|
||||
|
||||
All PostgreSQL features can be used with this option.
|
||||
|
||||
#### Transaction
|
||||
|
||||
This is the suggested option for serverless functions. A connection is only assigned to the client for the duration of a transaction. Two consecutive transactions from the same client
|
||||
could be executed over two different connections.
|
||||
|
||||
Some session-based PostgreSQL features such as prepared statements are not available with this option.
|
||||
A comprehensive list of incompatible features can be found [here](https://www.pgbouncer.org/features.html).
|
||||
|
||||
#### Statement
|
||||
|
||||
This is the most granular option. Connections are returned to the pool after every statement. Transactions with multiple statements are not allowed. This is best used when `AUTOCOMMIT` is in use.
|
||||
|
||||
### Finding the connection pool config
|
||||
Every Supabase project comes with PgBouncer for connection pooling. A connection pooler is useful for managing a large number of _temporary_ connections. For example, if you are using [Prisma](/docs/guides/integrations/prisma), Drizzle, Kysely, or anything deployed to a Serverless environment (AWS Lambdas or Edge Functions). You can find the connection pool config in the [Database settings](https://app.supabase.com/project/_/settings/database) inside the dashboard:
|
||||
|
||||
1. Go to the `Settings` section.
|
||||
2. Click `Database`.
|
||||
@@ -116,34 +51,134 @@ This is the most granular option. Connections are returned to the pool after eve
|
||||
/>
|
||||
</video>
|
||||
|
||||
## Choosing a connection method
|
||||
|
||||
- The Serverless APIs provide programmatic access and have built-in connection pooling. You can use these for all browser and application interactions. We recommend using these wherever possible.
|
||||
- A "direct connection" is Postgres' native connection system. You should use this for tools which are always alive - usually installed on a long-running server, like Node.js, Ruby, Python, etc.
|
||||
- A "connection pooler" is a tool which keeps connections "alive". You should use this for serverless functions and tools which disconnect from the database frequently, like Prisma, Drizzle, Kysely, etc.
|
||||
|
||||
Why would you use a connection pool? Primarily because the way that Postgres handles connections isn't very scalable for a large number of _temporary_ connections. You can use these simple questions to determine which connection method to use:
|
||||
|
||||
- Are you connecting to a database and _maintaining_ a connection? If yes, use a direct connection.
|
||||
- Are you connecting to your database and then _disconnecting_ immediately (e.g. a serverless environment)? If yes, use a connection pool.
|
||||
|
||||
## Connecting with SSL
|
||||
|
||||
Use this when connecting to your database to prevent snooping and man-in-the-middle attacks.
|
||||
You should connect to your database using SSL wherever possible, to prevent snooping and man-in-the-middle attacks.
|
||||
|
||||
You can obtain your connection info and Server root certificate from your application's dashboard:
|
||||
|
||||
Obtain your connection info and Server root certificate from your application’s dashboard.
|
||||

|
||||
|
||||
Assuming you’ve downloaded your certificate and it’s located at `$HOME/Downloads/prod-ca-2021.cer`, and your Host address is `db.abcdefghijklm.supabase.co` you can connect to the DB with
|
||||
SSL enabled as illustrated below:
|
||||
## How connection pooling works
|
||||
|
||||
1. With `psql`
|
||||
A "connection pool" is a system (external to Postgres) which manages connections, rather than PostgreSQL's native system. Supabase uses [PgBouncer](https://www.pgbouncer.org/) for connection pooling.
|
||||
|
||||
```
|
||||
psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-ca-2021.cer host=db.abcdefghijklm.supabase.co dbname=postgres user=postgres"
|
||||
When a client makes a request, PgBouncer "allocates" an available connection to the client. When the client transaction or session is completed the connection is returned to the pool and is free to be used by another client.
|
||||
|
||||

|
||||
|
||||
Pgbounce provides several Pool Modes, each handling connections differently:
|
||||
|
||||
#### Session
|
||||
|
||||
When a new client connects, a connection is assigned to the client until it disconnects. Afterward, the connection is returned back to the pool.
|
||||
|
||||
All PostgreSQL features can be used with this option.
|
||||
|
||||
#### Transaction
|
||||
|
||||
This is the suggested option for serverless functions. A connection is only assigned to the client for the duration of a transaction. Two consecutive transactions from the same client could be executed over two different connections.
|
||||
|
||||
Some session-based PostgreSQL features such as prepared statements are not available with this option. A comprehensive list of incompatible features can be found [here](https://www.pgbouncer.org/features.html).
|
||||
|
||||
#### Statement
|
||||
|
||||
This is the most granular option. Connections are returned to the pool after every statement. Transactions with multiple statements are not allowed. This is best used when `AUTOCOMMIT` is in use.
|
||||
|
||||
## Integrations
|
||||
|
||||
### Connecting with psql
|
||||
|
||||
[`psql`](https://www.postgresql.org/docs/current/app-psql.html) is a command-line tool that comes with Postgres.
|
||||
|
||||
Assuming you've downloaded your SSL certificate to `$HOME/Downloads/prod-supabase.cer`, and your host address is `db.ref.supabase.co` you connect to your database via SSL:
|
||||
|
||||
```shell
|
||||
psql "sslmode=verify-full sslrootcert=$HOME/Downloads/prod-supabase.cer host=db.ref.supabase.co dbname=postgres user=postgres"
|
||||
```
|
||||
|
||||
2. With `pgAdmin`
|
||||
a. Register a new Postgres server
|
||||

|
||||
### Connecting with pgAdmin
|
||||
|
||||
b. Name your server to your liking and add the connection info.
|
||||

|
||||

|
||||
[`pgAdmin`](https://www.pgadmin.org/) is a GUI tool for managing Postgres databases. You can use it to connect to your database via SSL:
|
||||
|
||||
3. Navigate to the SSL tab and change the SSL mode to Require. Next navigate to the Root certificate input, it will open up a
|
||||
file-picker modal. Select the certificate you downloaded from your Supabase dashboard and save the server details. PgAdmin
|
||||
should now be able to connect to your Postgres via SSL.
|
||||

|
||||
<StepHikeCompact>
|
||||
|
||||
<StepHikeCompact.Step step={1}>
|
||||
|
||||
<StepHikeCompact.Details title="Register">
|
||||
|
||||
Register a new Postgres server.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||

|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={2}>
|
||||
|
||||
<StepHikeCompact.Details title="Name">
|
||||
|
||||
Name your server.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||

|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Connect">
|
||||
|
||||
Add the connection info. You can use the "Direct connection" config, which you can find in your Supabase dashboard.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||

|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="SSL">
|
||||
|
||||
Navigate to the SSL tab and change the SSL mode to Require. Next navigate to the Root certificate input, it will open up a file-picker modal. Select the certificate you downloaded from your Supabase dashboard and save the server details. PgAdmin should now be able to connect to your Postgres via SSL.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||

|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
</StepHikeCompact>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ const { data, error } = await supabase
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Postgrest Operators](https://postgrest.org/en/stable/api.html#operators)
|
||||
- [Supabase API: JavaScript select](/docs/reference/javascript/select)
|
||||
- [Supabase API: JavaScript modifiers](/docs/reference/javascript/using-modifiers)
|
||||
|
||||
@@ -3,19 +3,15 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'vault',
|
||||
title: 'Vault',
|
||||
description: 'Use Supabase to store and serve files.',
|
||||
description:
|
||||
'Vault is a Postgres extension and accompanying Supabase UI that makes it safe and easy to store encrypted secrets.',
|
||||
subtitle: 'Managing secrets in Postgres.',
|
||||
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.
|
||||
@@ -31,79 +27,48 @@ Supabase provides a dashboard UI for the Vault that makes storing secrets easy.
|
||||
|
||||
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.
|
||||
## Secrets
|
||||
|
||||
## Deep Dive on How The Vault works
|
||||
You can use the Vault to store secrets - everything from Environment Variables to API Keys. You can then 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.
|
||||
|
||||
<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>
|
||||
## Encryption Keys
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Using the Vault
|
||||
|
||||
Using the vault is as simple as `INSERT`ing data into the
|
||||
`vault.secret` table.
|
||||
You can manage secrets and encryption keys from the UI or using SQL.
|
||||
|
||||
### Adding secrets
|
||||
|
||||
There is also a handy function for creating secrets called `vault.create_secret()`:
|
||||
|
||||
```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
|
||||
|
||||
select vault.create_secret('my_s3kre3t');
|
||||
```
|
||||
|
||||
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()`:
|
||||
<details>
|
||||
<summary>Show Result</summary>
|
||||
|
||||
```sql
|
||||
postgres=> select vault.create_secret('another_s3kre3t', 'unique_name', 'This is the description');
|
||||
-[ RECORD 1 ]-+-------------------------------------
|
||||
create_secret | 7095d222-efe5-4cd5-b5c6-5755b451e223
|
||||
create_secret | c9b00867-ca8b-44fc-a81d-d20b8169be17
|
||||
```
|
||||
|
||||
postgres=> select * from vault.secrets where id = '7095d222-efe5-4cd5-b5c6-5755b451e223';
|
||||
</details>
|
||||
|
||||
Secrets can also have an optional _unique_ name and an optional description. These are also arguments to `vault.create_secret()`:
|
||||
|
||||
```sql
|
||||
select vault.create_secret('another_s3kre3t', 'unique_name', 'This is the description');
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Show Result</summary>
|
||||
|
||||
```sql
|
||||
-[ RECORD 1 ]-----------------------------------------------------------------
|
||||
id | 7095d222-efe5-4cd5-b5c6-5755b451e223
|
||||
name | unique_name
|
||||
@@ -115,12 +80,49 @@ 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
|
||||
</details>
|
||||
|
||||
Alternatively, you can create a secret by `insert`ing data into the `vault.secret` table:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
insert into vault.secrets (secret)
|
||||
values ('s3kre3t_k3y') returning *;
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Show Result</summary>
|
||||
|
||||
```sql
|
||||
-[ 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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Viewing secrets
|
||||
|
||||
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:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```sql
|
||||
select *
|
||||
from vault.decrypted_secrets
|
||||
order by created_at desc
|
||||
limit 3;
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Show Result</summary>
|
||||
|
||||
```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
|
||||
@@ -153,17 +155,30 @@ created_at | 2022-12-14 02:29:21.3625+00
|
||||
updated_at | 2022-12-14 02:29:21.3625+00
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
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
|
||||
### 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');
|
||||
select
|
||||
vault.update_secret(
|
||||
'7095d222-efe5-4cd5-b5c6-5755b451e223',
|
||||
'n3w_upd@ted_s3kret',
|
||||
'updated_unique_name',
|
||||
'This is the updated description'
|
||||
);
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Show Result</summary>
|
||||
|
||||
```sql
|
||||
-[ RECORD 1 ]-+-
|
||||
update_secret |
|
||||
|
||||
@@ -180,6 +195,38 @@ created_at | 2022-12-14 02:34:23.85159+00
|
||||
updated_at | 2022-12-14 02:51:13.938396+00
|
||||
```
|
||||
|
||||
</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.
|
||||
|
||||
## 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.
|
||||
@@ -209,7 +256,7 @@ 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
|
||||
## Resources
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -24,16 +24,25 @@ jobs:
|
||||
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
PROJECT_ID: zdtdtxajzydjqzuktnqx
|
||||
PROJECT_ID: your-project-id
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: 1.0.0
|
||||
version: latest
|
||||
|
||||
- run: supabase functions deploy your-function-name --project-ref $PROJECT_ID
|
||||
- run: supabase functions deploy --project-ref $PROJECT_ID
|
||||
```
|
||||
|
||||
Since Supabase CLI [v1.62.0](https://github.com/supabase/cli/releases/tag/v1.62.0) you can deploy all functions with a single command.
|
||||
|
||||
Individual function configuration like [JWT verification](/docs/reference/cli/config#functions.function_name.verify_jwt) and [import map location](/docs/reference/cli/config#functions.function_name.import_map) can be set via the `config.toml` file.
|
||||
|
||||
```toml
|
||||
[functions.hello-world]
|
||||
verify_jwt = false
|
||||
```
|
||||
|
||||
<div class="video-container">
|
||||
|
||||
@@ -40,9 +40,18 @@ jobs:
|
||||
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: 1.0.0
|
||||
version: latest
|
||||
|
||||
- run: supabase functions deploy github-action-deploy --project-ref $PROJECT_ID
|
||||
- run: supabase functions deploy --project-ref $PROJECT_ID
|
||||
```
|
||||
|
||||
Since Supabase CLI [v1.62.0](https://github.com/supabase/cli/releases/tag/v1.62.0) you can deploy all functions with a single command.
|
||||
|
||||
Individual function configuration like [JWT verification](/docs/reference/cli/config#functions.function_name.verify_jwt) and [import map location](/docs/reference/cli/config#functions.function_name.import_map) can be set via the `config.toml` file.
|
||||
|
||||
```toml
|
||||
[functions.hello-world]
|
||||
verify_jwt = false
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
@@ -41,6 +41,8 @@ This creates a function stub in your `supabase` folder at `./functions/hello-wor
|
||||
|
||||
## Deploy to production
|
||||
|
||||
### Deploy a specific function
|
||||
|
||||
```bash
|
||||
supabase functions deploy hello-world
|
||||
```
|
||||
@@ -56,6 +58,21 @@ If you want to use Edge Functions to handle webhooks (e.g. [Stripe payment webho
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Deploy all functions
|
||||
|
||||
```bash
|
||||
supabase functions deploy
|
||||
```
|
||||
|
||||
Since Supabase CLI [v1.62.0](https://github.com/supabase/cli/releases/tag/v1.62.0) you can deploy all functions with a single command. This is useful for example when [deploying with GitHub Actions](/docs/guides/functions/cicd-workflow).
|
||||
|
||||
Individual function configuration like [JWT verification](/docs/reference/cli/config#functions.function_name.verify_jwt) and [import map location](/docs/reference/cli/config#functions.function_name.import_map) can be set via the `config.toml` file.
|
||||
|
||||
```toml
|
||||
[functions.hello-world]
|
||||
verify_jwt = false
|
||||
```
|
||||
|
||||
## Invoking remote functions
|
||||
|
||||
You can invoke Edge Functions using curl:
|
||||
|
||||
@@ -102,9 +102,9 @@ export const meta = {
|
||||
|
||||
export const useCases = [
|
||||
{
|
||||
title: 'OpenAI Vector Search',
|
||||
href: '/guides/getting-started/openai/vector-search',
|
||||
description: `Build your own custom ChatGPT with Next.js, OpenAI and pg_vector.`,
|
||||
title: 'AI, Vectors, and embeddings',
|
||||
href: '/docs/guides/ai#examples',
|
||||
description: `Build AI-enabled applications using our Vector toolkit.`,
|
||||
icon: '/docs/img/icons/openai_logo',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ The Dashboard provides a wide range of features for setting up your project: cre
|
||||
|
||||
2. **Easier Collaboration**: Developing locally can make it easier to collaborate with others on the same project.
|
||||
|
||||
3. **Cost-Effective**: Supabase provides a generous free tier and gives you two free projects to get started. But what if you need more than two? When you develop locally, you can spin up unlimited local projects and link them with live projects when you're ready to launch.
|
||||
3. **Cost-Effective**: Supabase provides a generous free plan and gives you two free projects to get started. But what if you need more than two? When you develop locally, you can spin up unlimited local projects and link them with live projects when you're ready to launch.
|
||||
|
||||
4. **Configuration in code**: If you directly change your tables via the Dashboard, none of that gets captured in code. If you follow these local development practices, you'll store all of your table schemas in code.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ In this guide, we will demonstrate how to create a new Supabase project, install
|
||||
|
||||

|
||||
|
||||
[Supabase](https://supabase.com/) is an open-source Firebase alternative that provides a PostgreSQL database, storage, authentication, and a dynamic REST API based on your schema. While it is possible to self-host Supabase on your own infrastructure, this article will focus on Supabase Cloud's Free tier, which is the fastest and easiest way to get started.
|
||||
[Supabase](https://supabase.com/) is an open-source Firebase alternative that provides a PostgreSQL database, storage, authentication, and a dynamic REST API based on your schema. While it is possible to self-host Supabase on your own infrastructure, this article will focus on Supabase Cloud's Free plan, which is the fastest and easiest way to get started.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ PolyScale provides caching for TCP connections and GraphQL. Support for caching
|
||||
|
||||
## Step 0: Create a PolyScale account
|
||||
|
||||
If you do not already have a PolyScale account, you can create an account [here](https://app.polyscale.ai/signup). PolyScale offers a free tier and no credit card is required.
|
||||
If you do not already have a PolyScale account, you can create an account [here](https://app.polyscale.ai/signup). PolyScale offers a free plan and no credit card is required.
|
||||
|
||||
## Step 1: Create your PolyScale Cache
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ The table below shows the corresponding permissions for each available role you
|
||||
prevents accidental invites to accounts not managed by your company's
|
||||
enterprise systems.
|
||||
|
||||
[^2]: Available on the Teams and Enterprise Tiers.
|
||||
[^2]: Available on the Teams and Enterprise Plans.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Daily Backups and PITR are mutually exclusive. If your project opts into using P
|
||||
|
||||
## Daily Backups
|
||||
|
||||
All Pro and Enterprise tier Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
|
||||
All Pro and Enterprise plan Supabase projects are backed up automatically on a daily basis. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data if disaster hits at the most inopportune time. If a lower RPO is required, enabling Point-in-Time Recovery should be considered.
|
||||
|
||||
<Admonition type="note">
|
||||
For security purposes, passwords for custom roles are not stored in daily backups, and will not be
|
||||
@@ -35,7 +35,7 @@ The PostgreSQL utility [pg_dumpall](https://www.postgresql.org/docs/current/app-
|
||||
|
||||

|
||||
|
||||
You can access daily backups in the [Scheduled backups](https://app.supabase.com/project/_/database/backups/scheduled) settings in the Dashboard. Pro tier projects can access the last 7 days’ worth of daily backups while Enterprise tier projects can access up to 30 days’ worth of daily backups. Users can restore their project to any one of the backups or download them as a zipped SQL file.
|
||||
You can access daily backups in the [Scheduled backups](https://app.supabase.com/project/_/database/backups/scheduled) settings in the Dashboard. Pro plan projects can access the last 7 days’ worth of daily backups while Enterprise plan projects can access up to 30 days’ worth of daily backups. Users can restore their project to any one of the backups or download them as a zipped SQL file.
|
||||
|
||||
### Restoration Process [#daily-backups-restoration-process]
|
||||
|
||||
@@ -52,7 +52,7 @@ The Dashboard will then prompt for a confirmation before proceeding with the res
|
||||
Point-in-Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. Even with daily backups, a day’s worth of data could still be lost. With PITR, backups could be performed up to the point of disaster.
|
||||
|
||||
<Admonition type="note">
|
||||
This feature is available to all Enterprise tier projects. Pro tier projects can enable PITR as an
|
||||
This feature is available to all Enterprise plan projects. Pro plan projects can enable PITR as an
|
||||
add-on.
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -8,18 +8,20 @@ export const meta = {
|
||||
|
||||
Every project on the Supabase Platform comes with its own dedicated Postgres instance running inside a virtual machine (VM). The following table describes the base instance with additional compute add-ons available if you need extra performance when scaling up Supabase.
|
||||
|
||||
| Plan | Pricing | CPU | Memory | Maximum Disk IO Bandwidth | Baseline Disk IO Bandwidth | Connections: Direct (recommended) | Connections: Pooler (recommended) |
|
||||
| --------------- | ------- | ----------------------- | ------ | ------------------------- | -------------------------- | --------------------------------- | --------------------------------- |
|
||||
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | 2,606 Mbps | 87 Mbps | 60 | 200 |
|
||||
| Small | $5 | 2-core ARM (shared) | 2 GB | 2,606 Mbps | 174 Mbps | 90 | 200 |
|
||||
| Medium | $50 | 2-core ARM (shared) | 4 GB | 2,606 Mbps | 347 Mbps | 120 | 200 |
|
||||
| Large | $100 | 2-core ARM (dedicated) | 8 GB | 4,750 Mbps | 630 Mbps | 160 | 300 |
|
||||
| XL | $200 | 4-core ARM (dedicated) | 16 GB | 4,750 Mbps | 1,188 Mbps | 240 | 700 |
|
||||
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | 4,750 Mbps | 2,375 Mbps | 380 | 1500 |
|
||||
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | 4,750 Mbps | 4,750 Mbps | 480 | 3000 |
|
||||
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | 9,500 Mbps | 9,500 Mbps | 490 | 6000 |
|
||||
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | 14,250 Mbps | 14,250 Mbps | 500 | 9000 |
|
||||
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | 19,000 Mbps | 19,000 Mbps | 500 | 12,000 |
|
||||
| Plan | Pricing | CPU | Memory | Connections: Direct | Connections: Pooler |
|
||||
| --------------- | ------- | ----------------------- | ------ | ------------------- | ------------------- |
|
||||
| Free (Included) | $0 | 2-core ARM (shared) | 1 GB | 60 | 200 |
|
||||
| Small | $5 | 2-core ARM (shared) | 2 GB | 90 | 200 |
|
||||
| Medium | $50 | 2-core ARM (shared) | 4 GB | 120 | 200 |
|
||||
| Large | $100 | 2-core ARM (dedicated) | 8 GB | 160 | 300 |
|
||||
| XL | $200 | 4-core ARM (dedicated) | 16 GB | 240 | 700 |
|
||||
| 2XL | $400 | 8-core ARM (dedicated) | 32 GB | 380 | 1500 |
|
||||
| 4XL | $950 | 16-core ARM (dedicated) | 64 GB | 480 | 3000 |
|
||||
| 8XL | $1,860 | 32-core ARM (dedicated) | 128 GB | 490 | 6000 |
|
||||
| 12XL | $2,790 | 48-core ARM (dedicated) | 192 GB | 500 | 9000 |
|
||||
| 16XL | $3,720 | 64-core ARM (dedicated) | 256 GB | 500 | 12,000 |
|
||||
|
||||
Number of connections above are recommended values.
|
||||
|
||||
[Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
|
||||
|
||||
@@ -31,11 +33,32 @@ All Postgres instances on Supabase are dedicated applications running inside ded
|
||||
|
||||
When considering compute upgrades, assess whether your bottlenecks are hardware-constrained or software-constrained. For example, you may want to look into [optimizing the number of connections](/docs/guides/platform/performance#optimizing-the-number-of-connections) or [examining query performance](/docs/guides/platform/performance#examining-query-performance). When you're happy with your Postgres instance's performance, then you can focus on additional compute resources. For example, you can load test your application in staging to understand your compute requirements. You can also start out on a smaller tier, [create a report](https://app.supabase.com/project/_/reports) in the Dashboard to monitor your CPU utilization, and upgrade later as needed
|
||||
|
||||
## Disk IO bandwidth
|
||||
## Disk Throughput and IOPS
|
||||
|
||||
SSD Disks are attached to your servers and the disk performance of your workload is determined by the Disk IO bandwidth of this connection. Smaller compute instances can burst up to the maximum disk IO bandwidth for 30 minutes in a day. Beyond that, the performance reverts to the baseline disk IO bandwidth. For example, the free tier can burst up to 2,606 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. If you need consistent disk performance, choose the 4XL or larger compute add-on which has the same baseline and maximum disk IO bandwidth.
|
||||
SSD Disks are attached to your servers and the disk performance depends on the compute add-on of your instance.
|
||||
|
||||
If you're unsure of how many IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://app.supabase.com/project/_/reports). If the `Daily Disk IO Budget % Remaining` stat is less than 100%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric drops to zero, the workload has used up all the burst IO throughput minutes during the day and is running at the baseline performance. These projects are good candidates for upgrading to a larger compute add with higher baseline throughput.
|
||||
| Plan | Pricing | Max Disk Throughput | Baseline Disk Throughput | Max IOPS | Baseline IOPS |
|
||||
| --------------- | ------- | ------------------- | ------------------------ | ----------- | ------------- |
|
||||
| Free (Included) | $0 | 2,085 Mbps | 87 Mbps | 11,800 IOPS | 500 IOPS |
|
||||
| Small | $5 | 2,085 Mbps | 174 Mbps | 11,800 IOPS | 1,000 IOPS |
|
||||
| Medium | $50 | 2,085 Mbps | 347 Mbps | 11,800 IOPS | 2,000 IOPS |
|
||||
| Large | $100 | 4,750 Mbps | 630 Mbps | 20,000 IOPS | 3,600 IOPS |
|
||||
| XL | $200 | 4,750 Mbps | 1,188 Mbps | 20,000 IOPS | 6,000 IOPS |
|
||||
| 2XL | $400 | 4,750 Mbps | 2,375 Mbps | 20,000 IOPS | 12,000 IOPS |
|
||||
| 4XL | $950 | 4,750 Mbps | 4,750 Mbps | 20,000 IOPS | 20,000 IOPS |
|
||||
| 8XL | $1,860 | 9,500 Mbps | 9,500 Mbps | 40,000 IOPS | 40,000 IOPS |
|
||||
| 12XL | $2,790 | 14,250 Mbps | 14,250 Mbps | 50,000 IOPS | 50,000 IOPS |
|
||||
| 16XL | $3,720 | 19,000 Mbps | 19,000 Mbps | 80,000 IOPS | 80,000 IOPS |
|
||||
|
||||
[Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
|
||||
|
||||
### Bursting and Daily Disk Budget
|
||||
|
||||
Smaller compute instances can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. For example, the free tier can burst up to 2,085 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. Your disk budget gets replenished throughout the day.
|
||||
|
||||
If you need consistent disk performance, choose the 4XL or larger compute add-on which has the same baseline and maximum disk throughput and IOPS.
|
||||
|
||||
If you're unsure of how much throughput or IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://app.supabase.com/project/_/reports). If the `Daily Disk IO Budget % Remaining` stat is less than 100%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric drops to zero, the workload has used up all the burst IO throughput minutes during the day and is running at the baseline performance. These projects are good candidates for upgrading to a larger compute add-on with higher baseline throughput.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const meta = {
|
||||
video: 'https://www.youtube.com/v/6rcGnW_Mh-0',
|
||||
}
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid plan](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -100,7 +100,7 @@ To get started:
|
||||
1. [Install](/docs/guides/resources/supabase-cli) the Supabase CLI 1.22.0+.
|
||||
1. [Log in](/docs/guides/resources/supabase-getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project you'd like to set up a vanity subdomain for.
|
||||
1. Ensure that your project has a paid subscription (Pro/Pay as you go/Enterprise tier) in the [Billing page of the Dashboard](https://app.supabase.com/project/_/settings/billing/subscription).
|
||||
1. Ensure that your project has a paid subscription (Pro/Pay as you go/Enterprise plan) in the [Billing page of the Dashboard](https://app.supabase.com/project/_/settings/billing/subscription).
|
||||
|
||||
## Configure a Vanity Subdomain
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Database Space is consumed primarily by your data, indexes, and materialized vie
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Depending on your billing tier, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Disk Management](#disk-management) section.
|
||||
Depending on your billing plan, your database can go into read-only mode which can prevent you inserting and deleting data. There are instructions for managing read-only mode in the [Disk Management](#disk-management) section.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -55,15 +55,15 @@ New Supabase projects have a database size of ~40-60mb. This space includes pre-
|
||||
|
||||
## Disk management
|
||||
|
||||
Supabase uses network-attached storage to balance performance with scalability. The behavior of your disk depends on your billing tier.
|
||||
Supabase uses network-attached storage to balance performance with scalability. The behavior of your disk depends on your billing plan.
|
||||
|
||||
### Paid Tier Behavior
|
||||
### Paid Plan Behavior
|
||||
|
||||
Pro and Enterprise projects have auto-scaling Disk Storage.
|
||||
|
||||
Disk Storage expands automatically when the database reaches 90% of the disk size. The disk is expanded to be 50% larger (e.g., 8GB -> 12GB). Auto-scaling can only take place once every 6 hours. If within those 6 hours you reach 95% of the disk space, your project will enter read-only mode.
|
||||
|
||||
The maximum Disk Storage Size for Pro Tier is 64TB. If you need more than this, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
|
||||
The maximum Disk Storage Size for Pro plan is 64TB. If you need more than this, [contact us](https://app.supabase.com/support/new) to learn more about the Enterprise plan.
|
||||
|
||||
The Disk Storage Size can also be manually expanded in [Database settings page](https://app.supabase.com/project/_/settings/database). The maximum default you can expand the Disk Storage to is 200GB. If you wish to manually expand it any more than 200B, please [contact us](https://app.supabase.com/support/new) to discuss expanding the 200GB limit.
|
||||
|
||||
@@ -73,11 +73,11 @@ You may want to import a lot of data into your database which requires multiple
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Free Tier Behavior
|
||||
### Free Plan Behavior
|
||||
|
||||
Free Tier projects enter [read-only](#read-only-mode) mode when you exceed the 500mb limit. Once in read-only mode, you have several options:
|
||||
Free Plan projects enter [read-only](#read-only-mode) mode when you exceed the 500mb limit. Once in read-only mode, you have several options:
|
||||
|
||||
- [Upgrade to the Pro or Enterprise tier](https://app.supabase.com/project/_/settings/billing/subscription) to enable auto-scaling and expand beyond the 500mb database size limit.
|
||||
- [Upgrade to the Pro or Enterprise plan](https://app.supabase.com/project/_/settings/billing/subscription) to enable auto-scaling and expand beyond the 500mb database size limit.
|
||||
- [Disable read-only mode](#disabling-read-only-mode) and reduce your database size.
|
||||
|
||||
### Read-only mode
|
||||
|
||||
@@ -42,14 +42,14 @@ After developing your project and deciding it's Production Ready, you should run
|
||||
- Use your own SMTP credentials so that you have full control over the deliverability of your transactional auth emails (see Auth > Settings)
|
||||
- you can grab SMTP credentials from any major email provider such as SendGrid, AWS SES, etc.
|
||||
- The default rate limit for auth emails provided by Supabase is 30 new users per hour, if doing a major public announcement you will likely require more than this.
|
||||
- If your application is on the free tier and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources.
|
||||
- If your application is on the free plan and is **not** expected to be queried at least once every 7 days, then it may be paused by Supabase to save on server resources.
|
||||
- You can restore paused projects from the Supabase dashboard.
|
||||
- Upgrade to Pro to guarantee that your project will not be paused for inactivity.
|
||||
- Database backups are not available for download on the free tier.
|
||||
- Database backups are not available for download on the free plan.
|
||||
- You can set up your own backup systems using tools like [pg_dump](https://www.postgresqltutorial.com/postgresql-backup-database/) or [wal-g](https://github.com/wal-g/wal-g).
|
||||
- Nightly backups for Pro tier projects are available on the Supabase dashboard for up to 7 days.
|
||||
- Nightly backups for Pro plan projects are available on the Supabase dashboard for up to 7 days.
|
||||
- Point in Time Recovery (PITR) allows a project to be backed up at much shorter intervals. This provides users an option to restore to any chosen point of up to seconds in granularity. In terms of Recovery Point Objective (RPO), Daily Backups would be suitable for projects willing to lose up to 24 hours worth of data. If a lower RPO is required, enable PITR.
|
||||
- Upgrading to the Supabase Pro Tier will give you [access to our support team](https://app.supabase.com/support/new).
|
||||
- Upgrading to the Supabase Pro plan will give you [access to our support team](https://app.supabase.com/support/new).
|
||||
|
||||
## Rate Limiting, Resource Allocation, & Abuse Prevention
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ The Supabase platform offers several HTTP APIs for each project. These APIs can
|
||||
|
||||
The project the request was being made against has been paused. The project cannot process requests until it is un-paused by the owner.
|
||||
|
||||
Free-tier projects may be paused due to inactivity, on request by the owner, or in rare instances, due to abuse.
|
||||
Free-plan projects may be paused due to inactivity, on request by the owner, or in rare instances, due to abuse.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export const meta = {
|
||||
description: 'Getting the best results out of your Supabase project',
|
||||
}
|
||||
|
||||
The Supabase platform automatically optimizes your Postgres database to take advantage of the compute resources of the tier your project is on. However, these optimizations are based on assumptions about the type of workflow the project is being utilized for, and it is likely that better results can be obtained by tuning the database for your particular workflow.
|
||||
The Supabase platform automatically optimizes your Postgres database to take advantage of the compute resources of the plan your project is on. However, these optimizations are based on assumptions about the type of workflow the project is being utilized for, and it is likely that better results can be obtained by tuning the database for your particular workflow.
|
||||
|
||||
## Examining Query Performance
|
||||
|
||||
@@ -124,7 +124,7 @@ Queries which are the most time consuming are not necessarily bad, you may have
|
||||
|
||||
Generally for most applications a small percentage of data is accessed more regularly than the rest. To make sure that your regularly accessed data is available, Postgres tracks your data access patterns and keeps this in its [shared_buffers](https://www.postgresql.org/docs/15/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY) cache.
|
||||
|
||||
Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk I/O limits](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io-bandwidth) causing significant performance issues.
|
||||
Applications with lower cache hit rates generally perform more poorly since they have to hit the disk to get results rather than serving them from memory. Very poor hit rates can also cause you to burst past your [Disk I/O limits](https://supabase.com/docs/guides/platform/compute-add-ons#disk-throughput-and-iops) causing significant performance issues.
|
||||
|
||||
You can view your cache and index hit rate by executing the following query:
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Some common solutions for this issue are:
|
||||
- [Using fewer Postgres connections](../platform/performance#configuring-clients-to-use-fewer-connections) can reduce the amount of resources needed on the project.
|
||||
- [Restarting](https://app.supabase.com/project/_/settings/general) the database. This only temporarily solves the issue by terminating any ongoing workloads that might be tying up your compute resources.
|
||||
|
||||
If your [Daily Disk IO budget](../platform/compute-add-ons#disk-io-bandwidth) has been drained, you will need to either wait for it to be replenished the next day, or upgrade to a larger compute add-on to increase the budget available to your project.
|
||||
If your [Daily Disk IO budget](../platform/compute-add-ons#disk-throughput-and-iops) has been drained, you will need to either wait for it to be replenished the next day, or upgrade to a larger compute add-on to increase the budget available to your project.
|
||||
|
||||
## Unable to connect to your Supabase Project
|
||||
|
||||
|
||||
@@ -45,8 +45,7 @@ The cache status of a particular request is sent in the `cf-cache-status` header
|
||||
## Smart CDN Caching
|
||||
|
||||
<Admonition type="note">
|
||||
Smart CDN caching is automatically enabled for [Pro tiers and
|
||||
above](https://supabase.com/pricing).
|
||||
Smart CDN caching is automatically enabled for [Pro plan and above](https://supabase.com/pricing).
|
||||
</Admonition>
|
||||
|
||||
With Smart CDN caching enabled, the asset metadata in your database is synchronized to the edge. This automatically revalidates the cache when the asset is changed or deleted.
|
||||
|
||||
@@ -11,7 +11,7 @@ export const meta = {
|
||||
Supabase Storage offers the functionality to transform and resize images dynamically. Any image stored in your buckets can be transformed and optimized for fast delivery.
|
||||
|
||||
<Admonition type="note">
|
||||
Image Resizing is currently in beta and enabled for [Pro tier and
|
||||
Image Resizing is currently in beta and enabled for [Pro plan and
|
||||
above](https://supabase.com/pricing).
|
||||
</Admonition>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="117" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect width="117" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h30v20H0z"/><path fill="#007ec6" d="M30 0h87v20H30z"/><path fill="url(#b)" d="M0 0h117v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><svg x="4px" y="0px" width="22px" height="20px" viewBox="-2 0 28 24" style="background-color: #fff;border-radius: 1px;"><path style="fill:#e8710a;" d="M1.977,16.77c-2.667-2.277-2.605-7.079,0-9.357C2.919,8.057,3.522,9.075,4.49,9.691c-1.152,1.6-1.146,3.201-0.004,4.803C3.522,15.111,2.918,16.126,1.977,16.77z"/><path style="fill:#f9ab00;" d="M12.257,17.114c-1.767-1.633-2.485-3.658-2.118-6.02c0.451-2.91,2.139-4.893,4.946-5.678c2.565-0.718,4.964-0.217,6.878,1.819c-0.884,0.743-1.707,1.547-2.434,2.446C18.488,8.827,17.319,8.435,16,8.856c-2.404,0.767-3.046,3.241-1.494,5.644c-0.241,0.275-0.493,0.541-0.721,0.826C13.295,15.939,12.511,16.3,12.257,17.114z"/><path style="fill:#e8710a;" d="M19.529,9.682c0.727-0.899,1.55-1.703,2.434-2.446c2.703,2.783,2.701,7.031-0.005,9.764c-2.648,2.674-6.936,2.725-9.701,0.115c0.254-0.814,1.038-1.175,1.528-1.788c0.228-0.285,0.48-0.552,0.721-0.826c1.053,0.916,2.254,1.268,3.6,0.83C20.502,14.551,21.151,11.927,19.529,9.682z"/><path style="fill:#f9ab00;" d="M4.49,9.691C3.522,9.075,2.919,8.057,1.977,7.413c2.209-2.398,5.721-2.942,8.476-1.355c0.555,0.32,0.719,0.606,0.285,1.128c-0.157,0.188-0.258,0.422-0.391,0.631c-0.299,0.47-0.509,1.067-0.929,1.371C8.933,9.539,8.523,8.847,8.021,8.746C6.673,8.475,5.509,8.787,4.49,9.691z"/><path style="fill:#f9ab00;" d="M1.977,16.77c0.941-0.644,1.545-1.659,2.509-2.277c1.373,1.152,2.85,1.433,4.45,0.499c0.332-0.194,0.503-0.088,0.673,0.19c0.386,0.635,0.753,1.285,1.181,1.89c0.34,0.48,0.222,0.715-0.253,1.006C7.84,19.73,4.205,19.188,1.977,16.77z"/></svg><text x="245" y="140" transform="scale(.1)" textLength="30"> </text><text x="725" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="770">Open in Colab</text><text x="725" y="140" transform="scale(.1)" textLength="770">Open in Colab</text></g> </svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 102 KiB |
@@ -58,13 +58,13 @@ The two services price quite differently, the key differences being:
|
||||
- [Supabase pricing is based around usage](https://supabase.com/pricing), so you only pay for what you use.
|
||||
- Heroku prices based on a tier model with [37 plans to choose from](https://elements.heroku.com/addons/heroku-postgresql#pricing).
|
||||
|
||||
Supabase’s free tier also includes a dedicated Postgres instance, and the best bit is you can upgrade to pro later without any interruptions.
|
||||
Supabase’s free plan also includes a dedicated Postgres instance, and the best bit is you can upgrade to pro later without any interruptions.
|
||||
|
||||
### Global Deployments
|
||||
|
||||
You may have strict data regulations that you must comply with, so choosing your region can be very important. Here’s how the deployment options stack up:
|
||||
|
||||
- Supabase can be deployed to any one of [12 data centers across the globe](https://github.com/supabase/supabase/discussions/4815#discussioncomment-1915129) (free tier included).
|
||||
- Supabase can be deployed to any one of [12 data centers across the globe](https://github.com/supabase/supabase/discussions/4815#discussioncomment-1915129) (free plan included).
|
||||
- Since Supabase is fully open source - you can also [self host wherever you like](https://supabase.com/docs/guides/hosting/overview).
|
||||
- You can deploy Heroku Postgres to two data centers (US and Europe) however [6 more data centers](https://devcenter.heroku.com/articles/regions) are available on the Enterprise plan.
|
||||
|
||||
|
||||
@@ -133,4 +133,4 @@ We look forward to showing off some more Supabase + Replit examples.
|
||||
|
||||
You can find my example repl here: [https://repl.it/@awalias/supabase-test#index.js](https://repl.it/@awalias/supabase-test#index.js)
|
||||
|
||||
Supabase has a free tier, head over to [https://app.supabase.com](https://app.supabase.com) to get started.
|
||||
Supabase has a free plan, head over to [https://app.supabase.com](https://app.supabase.com) to get started.
|
||||
@@ -47,15 +47,15 @@ We're announcing pricing today. If you don’t care for the details, [here is th
|
||||
|
||||
The [blog post here](/blog/pricing) goes into depth around our decision-making process.
|
||||
|
||||
The key takeaway is that we will stick with (what we feel is) a generous free tier. This was critical for many of our users who are still in the “build” phase of their project/business.
|
||||
The key takeaway is that we will stick with (what we feel is) a generous free plan. This was critical for many of our users who are still in the “build” phase of their project/business.
|
||||
|
||||
Another key takeaway is predictability. We’ve talked to too many devs who have been caught out by Firebase’s usage billing.
|
||||
|
||||
Usually this is due to rogue API requests, which have a habit of spiking without any good debugging tools. To circumvent this, our usage pricing is centered on more “predictable” mechanisms (like storage), avoiding usage-billing on items like API requests.
|
||||
|
||||
Even on our free tier you’re able to make millions of requests per day (we have developers doing this already!). In the future we will introduce “usage caps” on our top tier, which will allow you to specify maximum spends. Finally, we’re starting with soft-tiers, so nobody will be “unexpectedly” upgraded.
|
||||
Even on our free plan you’re able to make millions of requests per day (we have developers doing this already!). In the future we will introduce “usage caps” on our top tier, which will allow you to specify maximum spends. Finally, we’re starting with soft-tiers, so nobody will be “unexpectedly” upgraded.
|
||||
|
||||
We’re a startup ourselves, so we want to build a sustainable business. Even though we are offering a free tier, we’re confident in our ability to support it long-term with the enterprise features we are building (SSO, multi-cloud).
|
||||
We’re a startup ourselves, so we want to build a sustainable business. Even though we are offering a free plan, we’re confident in our ability to support it long-term with the enterprise features we are building (SSO, multi-cloud).
|
||||
|
||||
If you aren’t confident in our business model though, we’re open source. You can [host it yourself](https://github.com/supabase/supabase/tree/master/docker). Even better, everything we build is centered around Postgres, so you can pretty much `pgdump` your database and take it to your favorite Postgres platform.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ date: '03-29-2021'
|
||||
|
||||
## Pricing is hard
|
||||
|
||||
Many developers have been waiting for [pricing](/pricing) before building on Supabase. It has taken this long to announce pricing for one simple reason: pricing is hard. We've spent countless hours discussing and testing different models. We consulted users, OSS veterans, and SaaS Gurus. We explored and ruled out a number of options including: no free tier, pure usage based, pay-per-seat, pay-per-row, and "bolt-on" feature based pricing.
|
||||
Many developers have been waiting for [pricing](/pricing) before building on Supabase. It has taken this long to announce pricing for one simple reason: pricing is hard. We've spent countless hours discussing and testing different models. We consulted users, OSS veterans, and SaaS Gurus. We explored and ruled out a number of options including: no free plan, pure usage based, pay-per-seat, pay-per-row, and "bolt-on" feature based pricing.
|
||||
|
||||
Supabase is a suite of integrated products - should we compare our Postgres databases with Amazon RDS? Do we benchmark our Auth pricing against Auth0 or Okta?
|
||||
|
||||
@@ -19,11 +19,11 @@ So we decided to do what we always do: ship it, share it, and then listen to our
|
||||
|
||||
## TLDR
|
||||
|
||||
We are launching [pricing](/pricing) with three tiers:
|
||||
We are launching [pricing](/pricing) with three plans:
|
||||
|
||||
1. Free - for up to four hobby-level projects
|
||||
2. Pro - $25 per project per month, for more serious projects
|
||||
3. Usage based - for those who need more resource than what's on the Pro tier
|
||||
3. Usage based - for those who need more resource than what's on the Pro plan
|
||||
|
||||
## Goals for our pricing structure
|
||||
|
||||
@@ -33,7 +33,7 @@ Our goals for pricing are simple:
|
||||
- Price based on predictable metrics (no shock bills at the end of the month)
|
||||
- Enable and grow with our most successful users, providing a pricing model that supports their growth
|
||||
|
||||
We came up with a 3-tier model to offer predictability, and then iterated through numerous user interviews to answer the questions: "how much do we charge in each tier, and what features go in each tier?". To answer these questions we considered:
|
||||
We came up with a 3-plan model to offer predictability, and then iterated through numerous user interviews to answer the questions: "how much do we charge in each plan, and what features go in each plan?". To answer these questions we considered:
|
||||
|
||||
1. How much value does Supabase create compared to alternatives including Firebase, authentication providers like Okta and Auth0, API, and storage providers?
|
||||
2. How much time does Supabase save developers?
|
||||
@@ -41,13 +41,13 @@ We came up with a 3-tier model to offer predictability, and then iterated throug
|
||||
4. As developers ourselves, how do we feel about our proposed pricing?
|
||||
5. The underlying cost of development and maintenance of Supabase.
|
||||
|
||||
As a result, we've arrived at a pricing model that brings predictability, is sustainable, and enables us to continue to offer a free tier.
|
||||
As a result, we've arrived at a pricing model that brings predictability, is sustainable, and enables us to continue to offer a free plan.
|
||||
|
||||
Our model allows anybody to come in and experience the full power of the Supabase stack without first needing to put down a credit card.
|
||||
|
||||
## Launching with a free tier
|
||||
## Launching with a free plan
|
||||
|
||||
Many of our early users are building or migrating their passion projects to Supabase. Offering a free tier enables this. We also support a large number of students learning SQL for the first time, and fledgling startups which haven't started earning any revenue yet.
|
||||
Many of our early users are building or migrating their passion projects to Supabase. Offering a free plan enables this. We also support a large number of students learning SQL for the first time, and fledgling startups which haven't started earning any revenue yet.
|
||||
|
||||
Developers need time to build their side projects, and they don't want to pay for a database while they do it. At the same time, hosting Postgres databases is expensive. So we decided to take the same strategy that we always have: ship, share it, and iterate.
|
||||
|
||||
@@ -61,15 +61,15 @@ We will issue credits for those who signed up during alpha, beta, and those who
|
||||
|
||||
We know that Firebase pricing can be [problematic](https://medium.com/madhash/how-not-to-get-a-30k-bill-from-firebase-37a6cb3abaca).
|
||||
|
||||
That's why we made our pricing as predictable as possible. You can quickly understand which pricing tier you fit into, without having to forecast how many API requests you're going to make this quarter.
|
||||
That's why we made our pricing as predictable as possible. You can quickly understand which pricing plan you fit into, without having to forecast how many API requests you're going to make this quarter.
|
||||
|
||||
What does this mean for you as a developer?
|
||||
|
||||
- You can make as many API calls as you need to (within the constraints of your database). We already have users making millions of requests per day.
|
||||
- We have been deliberately generous with authentication and storage on the free tier so that you get as much of the Supabase experience as possible.
|
||||
- We have been deliberately generous with authentication and storage on the free plan so that you get as much of the Supabase experience as possible.
|
||||
- We're starting with soft-limits for usage billing to prevent unexpected charges.
|
||||
- We'll reach out to you before changing your billing plan to see if it makes sense for your project.
|
||||
- On our "Pay as you go" tier we will work with you to forecast billing before making a commitment.
|
||||
- On our "Pay as you go" plan we will work with you to forecast billing before making a commitment.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Now you can run Supabase locally in the terminal with `supabase start`. We hav
|
||||
|
||||
## Pricing
|
||||
|
||||
Our most frequently asked question by far is "ok supabase is sweet, but how much is it going to cost?". TL;DR there's **Free Tier** up to 500mb + 10k auth users, a **Pro** **Tier** at $25/month for 8GB + 100k auth users, and anything additional is charged on a usage basis.
|
||||
Our most frequently asked question by far is "ok supabase is sweet, but how much is it going to cost?". TL;DR there's **Free Plan** up to 500mb + 10k auth users, a **Pro** **Plan** at $25/month for 8GB + 100k auth users, and anything additional is charged on a usage basis.
|
||||
|
||||
See [Pricing Page](/pricing) for full details and also our blog on why [pricing is hard](/blog/pricing).
|
||||
|
||||
|
||||
@@ -86,4 +86,4 @@ Migrating to Supabase helped the team at Mobbin instantly improve the end-user e
|
||||
They now add new features with confidence, and they continue to help creators all over the globe find inspiration for their next project.
|
||||
You can [check out Mobbin on their website](https://mobbin.design/browse/ios/apps).
|
||||
|
||||
Sign up to Supabase's free tier and set up a scalable backend in less than 2 minutes.
|
||||
Sign up to Supabase's free plan and set up a scalable backend in less than 2 minutes.
|
||||
@@ -74,4 +74,4 @@ Replenysh clients include brands with global recognition and millions of custome
|
||||
|
||||
## Supabase help Replenysh continue to scale
|
||||
|
||||
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free tier!.](https://app.supabase.com/)
|
||||
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free plan!.](https://app.supabase.com/)
|
||||
@@ -138,7 +138,7 @@ Another nice side effect of making PostgreSQL do the heavy lifting is that Graph
|
||||
|
||||
## Performance
|
||||
|
||||
Each [free tier database](https://supabase.com/pricing) on the Supabase platform runs on a dedicated AWS t4g.micro instance with 2 vCPUs and 1 GB of memory.
|
||||
Each [free plan database](https://supabase.com/pricing) on the Supabase platform runs on a dedicated AWS t4g.micro instance with 2 vCPUs and 1 GB of memory.
|
||||
|
||||
To squeeze the most out of that limited hardware we had to make a few significant optimizations:
|
||||
|
||||
@@ -216,7 +216,7 @@ Batching similar operations reduces network round-trips and time spent in the da
|
||||
|
||||
**Result**
|
||||
|
||||
As a result of these optimizations, the throughput of a “hello world” equivalent query on Supabase free-tier hardware is:
|
||||
As a result of these optimizations, the throughput of a “hello world” equivalent query on Supabase free-plan hardware is:
|
||||
|
||||
- 377.4 requests/second through the API (mean)
|
||||
- 656.2 queries/second through SQL (single connection, mean)
|
||||
|
||||
@@ -119,7 +119,7 @@ As our platform continues its rapid adoption within the developer community, we'
|
||||
|
||||
## Enterprise features for everybody
|
||||
|
||||
Some features are too good to limit to large customers, so today we're introducing a few enterprise features into the Pro tier.
|
||||
Some features are too good to limit to large customers, so today we're introducing a few enterprise features into the Pro plan.
|
||||
|
||||
### Spend caps
|
||||
|
||||
@@ -130,10 +130,10 @@ Some features are too good to limit to large customers, so today we're introduci
|
||||
/>
|
||||
</video>
|
||||
|
||||
To simplify pricing, we've merged the “Pro” and “Pay as you go” tiers and introduced monthly spend caps to avoid nasty billing surprises.
|
||||
To simplify pricing, we've merged the “Pro” and “Pay as you go” plans and introduced monthly spend caps to avoid nasty billing surprises.
|
||||
These changes are to keep Supabase pricing [predictable, transparent, and developer friendly](/blog/pricing).
|
||||
|
||||
When you upgrade to the Pro Tier, spend caps are turned on by default, limiting your per-project costs to $25 per month.
|
||||
When you upgrade to the Pro Plan, spend caps are turned on by default, limiting your per-project costs to $25 per month.
|
||||
We're also retaining our soft limits while we manage the transition to granular spend-caps, so your service will continue to run even if your usage exceeds $25 (we'll contact you directly when you go over the limit). Right now there is a global project spend-cap, and in the future you'll have full control with configurable spend-caps on a “per-feature” basis.
|
||||
|
||||
### Database Add-ons
|
||||
@@ -167,9 +167,9 @@ And if you're new to SQL, we have plenty of templates included.
|
||||
This is just one of the exciting features we are releasing through our [Logflare acquisition](/blog/supabase-acquires-logflare).
|
||||
Log history is available to every Supabase project [^1]:
|
||||
|
||||
- Free Tier: 1 day of log history
|
||||
- Pro Tier: 7 days of log history
|
||||
- Enterprise Tier: 90 days of log history
|
||||
- Free Plan: 1 day of log history
|
||||
- Pro Plan: 7 days of log history
|
||||
- Enterprise Plan: 90 days of log history
|
||||
|
||||
### Elixir Livebooks
|
||||
|
||||
@@ -205,7 +205,7 @@ If you want to monitor your own PgBouncer connections you can easily [spin up a
|
||||
|
||||
## Enterprise Features
|
||||
|
||||
With the release of our new Enterprise Tier, we're announcing a tonne of new features for Enterprise customers.
|
||||
With the release of our new Enterprise Plan, we're announcing a tonne of new features for Enterprise customers.
|
||||
|
||||
### Point-in-Time Recovery
|
||||
|
||||
@@ -227,7 +227,7 @@ If you want access to the Prometheus Endpoint, [contact the Supabase Enterprise
|
||||
### SLAs & Enterprise Support
|
||||
|
||||
For our enterprise customers we know that service level agreements and support response times are also critical features. We continue to treat support as an
|
||||
important priority for all tiers, though enterprise users require confidence that our response times meet their business needs. We now offer faster response times for Enterprise customers,
|
||||
important priority for all plans, though enterprise users require confidence that our response times meet their business needs. We now offer faster response times for Enterprise customers,
|
||||
alongside Priority and Priority Plus support packages for those who need more comprehensive support. You can find further details around our SLAs & support
|
||||
[in our documentation](/docs/company/sla).
|
||||
|
||||
@@ -235,7 +235,7 @@ alongside Priority and Priority Plus support packages for those who need more co
|
||||
|
||||

|
||||
|
||||
If you need more information on our Enterprise tier, Pricing, SLAs, Support packages, or want to learn more about how Supabase can meet your scaling needs just
|
||||
If you need more information on our Enterprise plan, Pricing, SLAs, Support packages, or want to learn more about how Supabase can meet your scaling needs just
|
||||
[contact us](/contact/enterprise).
|
||||
|
||||
## Coming soon
|
||||
|
||||
@@ -131,7 +131,7 @@ Data visualization empowers organizations to turn unused data into actionable in
|
||||
|
||||

|
||||
|
||||
With our [free tier Supabase account](https://app.supabase.com/), you can start a new project today and use Metabase to visualize your app data.
|
||||
With our [free plan Supabase account](https://app.supabase.com/), you can start a new project today and use Metabase to visualize your app data.
|
||||
|
||||
If you have any questions please reach out via [Twitter](https://twitter.com/supabase) or join our [Discord](https://discord.supabase.com).
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ You will be prompted to sign in using your Github account with a big green butto
|
||||
|
||||

|
||||
|
||||
You will be entering a few things here like the name of the project. You can call it “chat” for now. For the database password, go ahead and hit the “Generate a password” button to generate a random password. We won't use this password in this app, but if you ever need it, you can always override it later to whatever you want it to be. You can leave the pricing plan for free as Supabase has a very generous free tier that will be way more than enough for our chat app. Once you have entered everything, you can press the “Create new Project” button. Spinning up a brand new Supabase project could take a few minutes.
|
||||
You will be entering a few things here like the name of the project. You can call it “chat” for now. For the database password, go ahead and hit the “Generate a password” button to generate a random password. We won't use this password in this app, but if you ever need it, you can always override it later to whatever you want it to be. You can leave the pricing plan for free as Supabase has a very generous free plan that will be way more than enough for our chat app. Once you have entered everything, you can press the “Create new Project” button. Spinning up a brand new Supabase project could take a few minutes.
|
||||
|
||||
Once your project is ready, we can dive into setting up our project!
|
||||
|
||||
|
||||
@@ -414,9 +414,9 @@ if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
Of course, we aren't doing proper Python example if we don't make a test notebook available:
|
||||
Of course, we aren't doing a proper Python example if we don't make a test notebook available:
|
||||
|
||||
[](https://colab.research.google.com/github/mansueli/SlackConsolidate/blob/main/SlackConsolidate_bot.ipynb)
|
||||
[Open in Colab](https://colab.research.google.com/github/mansueli/SlackConsolidate/blob/main/SlackConsolidate_bot.ipynb)
|
||||
|
||||
## Conclusion
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Today, we're excited to announce the general availability of Realtime's multipla
|
||||
Here are the key takeaways:
|
||||
|
||||
- We've added [Broadcast](https://supabase.com/docs/guides/realtime/broadcast) and [Presence](https://supabase.com/docs/guides/realtime/presence) to our Realtime server. You can use these features with the new [supabase-js](https://supabase.com/docs/reference/javascript/next) release.
|
||||
- All active Supabase projects on the free tier have access to the these features.
|
||||
- All active Supabase projects on the free plan have access to the these features.
|
||||
- All new Supabase projects created from August 18th have access to these features.
|
||||
- We will work with all other projects to migrate to the new Realtime over the next few weeks. If you want immediate access, [reach out](https://app.supabase.com/support/new).
|
||||
- [Chris McCord](https://twitter.com/chris_mccord), the creator of Phoenix Framework, is now a technical advisor.
|
||||
|
||||
@@ -64,7 +64,7 @@ We're making some changes to the way the Dashboard interacts with your database.
|
||||
|
||||

|
||||
|
||||
In case you missed it, Heroku is shutting down their free tier. We're big fans of Heroku and take a lot of inspiration from what they've built. Supabase is compatible with Heroku's PostgreSQL product (because we're just Postgres too), and if you have a free project running on Heroku we've created a [tool to help migrate](https://migrate.supabase.com/) to our free tier.
|
||||
In case you missed it, Heroku is shutting down their free plan. We're big fans of Heroku and take a lot of inspiration from what they've built. Supabase is compatible with Heroku's PostgreSQL product (because we're just Postgres too), and if you have a free project running on Heroku we've created a [tool to help migrate](https://migrate.supabase.com/) to our free plan.
|
||||
|
||||
## Made with Supabase
|
||||
|
||||
|
||||
@@ -85,13 +85,13 @@ Behind the scenes, we've provided a few small niceties:
|
||||
|
||||
<div className="bg-gray-300 rounded-lg p-6 italic">
|
||||
|
||||
Image Resizing is in Beta. While in Beta, it is only enabled for Pro-tier and Enterprise customers.
|
||||
Image Resizing is in Beta. While in Beta, it is only enabled for Pro-plan and Enterprise customers.
|
||||
|
||||
</div>
|
||||
|
||||
## Smart CDN
|
||||
|
||||
Resizing images can get expensive, so before this release we knew we needed a better caching system. We already provide a “basic” CDN for all projects (including Free-tier), so the important update here is some smarter cache invalidation whenever there are changes.
|
||||
Resizing images can get expensive, so before this release we knew we needed a better caching system. We already provide a “basic” CDN for all projects (including Free-plan), so the important update here is some smarter cache invalidation whenever there are changes.
|
||||
|
||||

|
||||
|
||||
@@ -179,7 +179,7 @@ The green line here shows the number of cache hits to our CDN. As you can see, w
|
||||
|
||||
## Availability and Pricing
|
||||
|
||||
Image Resizing is in Beta. While in Beta, it is only enabled for Pro-tier and Enterprise customers.
|
||||
Image Resizing is in Beta. While in Beta, it is only enabled for Pro-plan and Enterprise customers.
|
||||
|
||||
When choosing a pricing model there were a few options that competitors offered:
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ Since our first platform release, v0.2.1, the feature set of pg_graphql has stea
|
||||
|
||||
## Background
|
||||
|
||||
pg_graphql was created to satisfy an extreme set of constraints. Mainly, [Supabase free tier](https://supabase.com/pricing) projects run on servers with 1 GB of memory. On those servers, we squeeze tuned versions of [PostgreSQL](https://github.com/supabase/postgres), [PostgREST](https://postgrest.org/en/stable/), and [GoTrue](https://github.com/supabase/gotrue). Every megabyte consumed by something that isn't PostgreSQL is another chance for an index to fall out of memory, or a large query to fail.
|
||||
pg_graphql was created to satisfy an extreme set of constraints. Mainly, [Supabase free plan](https://supabase.com/pricing) projects run on servers with 1 GB of memory. On those servers, we squeeze tuned versions of [PostgreSQL](https://github.com/supabase/postgres), [PostgREST](https://postgrest.org/en/stable/), and [GoTrue](https://github.com/supabase/gotrue). Every megabyte consumed by something that isn't PostgreSQL is another chance for an index to fall out of memory, or a large query to fail.
|
||||
|
||||
Our philosophy when adding to the stack is to use existing open source tools wherever possible. We surveyed the available GraphQL → SQL options and found some excellent candidates in Hasura and Graphile. Both support the set of features we're interested in, but consume significantly more memory than we could sacrifice on the free tier. Realizing that, we searched for an architecture that could meet our runtime constraints and performance requirements.
|
||||
Our philosophy when adding to the stack is to use existing open source tools wherever possible. We surveyed the available GraphQL → SQL options and found some excellent candidates in Hasura and Graphile. Both support the set of features we're interested in, but consume significantly more memory than we could sacrifice on the free plan. Realizing that, we searched for an architecture that could meet our runtime constraints and performance requirements.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -60,7 +60,7 @@ Somewhere around the 4,000 line count, things started to bog down. The rough sha
|
||||
- SQL functions are “pass by value” (copy) so passing state around can get expensive
|
||||
- The query planner tends to stop inlining functions when calls are deeply nested
|
||||
|
||||
Combine those two facts with our GraphQL AST being a big ball of JSONB, and the GitHub issues about slow schema introspection on free tier started to trickle in.
|
||||
Combine those two facts with our GraphQL AST being a big ball of JSONB, and the GitHub issues about slow schema introspection on free plan started to trickle in.
|
||||
|
||||
Enter [pgx](https://github.com/tcdi/pgx)…
|
||||
|
||||
@@ -68,7 +68,7 @@ pgx is a rust framework for building Postgres extensions. It has a very polished
|
||||
|
||||
One of rust's main selling points is zero cost abstractions. That means high-level concepts like generics incur no runtime penalty. Transitioning from SQL's expensive abstractions to rust's zero cost abstractions has been hugely satisfying as it enabled refactoring the codebase into a more maintainable state. Development velocity is up. Code quality is up. Performance is WAY up.
|
||||
|
||||
As of 1.0, overhead introduced by pg_graphql is sub 300 *micro*seconds per request on free-tier hardware. When executed from SQL we see ~1060 queries per second per connection (no parallelism). If we include the entire Auth + HTTP stack, free tier can handle ~645 requests per second. On larger instance the stack handles upwards of 10k requests / second.
|
||||
As of 1.0, overhead introduced by pg_graphql is sub 300 *micro*seconds per request on free-plan hardware. When executed from SQL we see ~1060 queries per second per connection (no parallelism). If we include the entire Auth + HTTP stack, free plan can handle ~645 requests per second. On larger instance the stack handles upwards of 10k requests / second.
|
||||
|
||||
```text
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
|
||||
|
||||
@@ -15,7 +15,7 @@ toc_depth: 3
|
||||
|
||||
When we announced Point-in-Time a few months ago, it was only available for Enterprise customers. Today, we're making PITR available for more projects, with a new Dashboard UI that makes it simple to use.
|
||||
|
||||
Pro-tier projects created from August 2022 onwards (excluding Northeast Asia / Seoul region), can now enable Point In Time Recovery (PITR) from the dashboard. Projects must have the "Small" compute add-on to ensure that PITR operates smoothly behind the scenes.
|
||||
Pro-plan projects created from August 2022 onwards (excluding Northeast Asia / Seoul region), can now enable Point In Time Recovery (PITR) from the dashboard. Projects must have the "Small" compute add-on to ensure that PITR operates smoothly behind the scenes.
|
||||
|
||||
For older projects, don't worry - we're working on making it available for everybody. In the meantime, if you would like to have PITR enabled for your ineligible project, reach out directly [here](https://app.supabase.com/support/new), or stay tuned for further updates.
|
||||
|
||||
@@ -35,7 +35,7 @@ Under the hood, we use [WAL-G](https://github.com/wal-g/wal-g), an open source a
|
||||
|
||||
Consider your Recovery Point Objective (RPO) when deciding whether to enable Point in Time Recovery. RPO is the threshold for how much data, measured in time, a business could lose when disaster strikes. This is dependent on a business and its underlying requirements. The agreed upon RPO would be a deciding factor in choosing which solution best fits a project.
|
||||
|
||||
While all Pro-tier projects and above are backed up on a daily basis, this means that at the worst case, a project could lose up to 24 hours worth of data if disaster hits at the most inopportune time. With Point in Time Recovery however, backups are made at much shorter intervals, shortening the RPO. WAL files are backed up at two minute intervals. This could be faster if it hits a certain file threshold before the the two minute mark.
|
||||
While all Pro-plan projects and above are backed up on a daily basis, this means that at the worst case, a project could lose up to 24 hours worth of data if disaster hits at the most inopportune time. With Point in Time Recovery however, backups are made at much shorter intervals, shortening the RPO. WAL files are backed up at two minute intervals. This could be faster if it hits a certain file threshold before the the two minute mark.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ Privacy is becoming one of the most important features in a modern product. Supa
|
||||
|
||||
### Using the Vault
|
||||
|
||||
You'll see the Vault appear in the Dashboard over the next month. If you want to use Vault today, email growth@supabase.com and we'll enable it on your account. Vault is available for all customers (including the Free-tier).
|
||||
You'll see the Vault appear in the Dashboard over the next month. If you want to use Vault today, email growth@supabase.com and we'll enable it on your account. Vault is available for all customers (including the Free-plan).
|
||||
|
||||
## More Launch Week 6
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Resumable uploads divides the file into chunks before uploading them, emitting p
|
||||
/>
|
||||
</video>
|
||||
|
||||
With this release, users on the Pro tier or higher can now upload files as large as 50GB! This substantial upgrade from the previous limit of 5GB offers even more flexibility for your file uploads.
|
||||
With this release, users on the Pro plan or higher can now upload files as large as 50GB! This substantial upgrade from the previous limit of 5GB offers even more flexibility for your file uploads.
|
||||
|
||||
To build this feature, we implemented Postgres Advisory locks which solved some gnarly concurrency problems. We can now handle edge-cases, like two clients uploading to the same location. We’ll deep dive into how we implemented Advisory locks later in the post.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ With Single Sign-On (SSO), your users can login with their company's identity pr
|
||||
|
||||
Every developer building a B2B application eventually needs the SSO authentication flow to onboard enterprise customers. SSO is a requirement for larger Enterprise customers because it's a standard request in Enterprise Security Policies. Over the past few months, we've been [dogfooding SSO for our own Enterprise customers](https://supabase.com/docs/guides/platform/sso), and today we're releasing it for you to do the same.
|
||||
|
||||
Building SSO into your application isn't necessarily hard, but does come with some complexity. A lot of time can be spent understanding the nuances and details of the protocol - from dissecting the jargon to testing the implementation heavily. It took us months to build it for ourselves. With this release, you will have SSO set up and running in less than an hour so that you can focus on shipping the core features of your product. This feature is available for the [Pro-tier and above](https://supabase.com/pricing), starting today. This will also be available on the self-hosted version.
|
||||
Building SSO into your application isn't necessarily hard, but does come with some complexity. A lot of time can be spent understanding the nuances and details of the protocol - from dissecting the jargon to testing the implementation heavily. It took us months to build it for ourselves. With this release, you will have SSO set up and running in less than an hour so that you can focus on shipping the core features of your product. This feature is available for the [Pro-plan and above](https://supabase.com/pricing), starting today. This will also be available on the self-hosted version.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
|
||||
@@ -98,4 +98,4 @@ Migrating to Supabase helped the team at Mobbin instantly improve the end-user e
|
||||
They now add new features with confidence, and they continue to help creators all over the globe find inspiration for their next project.
|
||||
You can [check out Mobbin on their website](https://mobbin.design/browse/ios/apps).
|
||||
|
||||
[Sign up](https://app.supabase.com/) for Supabase's free tier and set up a scalable backend in less than 2 minutes.
|
||||
[Sign up](https://app.supabase.com/) for Supabase's free plan and set up a scalable backend in less than 2 minutes.
|
||||
@@ -88,4 +88,4 @@ Replenysh clients include brands with global recognition and millions of custome
|
||||
|
||||
## Supabase help Replenysh continue to scale
|
||||
|
||||
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free tier!.](https://app.supabase.com)
|
||||
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free plan!.](https://app.supabase.com)
|
||||
@@ -17,6 +17,7 @@ export interface CodeBlockProps {
|
||||
className?: string
|
||||
children?: string
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
background?: string
|
||||
}
|
||||
|
||||
function CodeBlock(props: CodeBlockProps) {
|
||||
@@ -98,7 +99,7 @@ function CodeBlock(props: CodeBlockProps) {
|
||||
padding: '21px 24px',
|
||||
fontSize: large ? 18 : '0.875rem',
|
||||
lineHeight: large ? 1.6 : 1.4,
|
||||
background: '#181818',
|
||||
background: props.background ?? '#1A1A1A',
|
||||
}}
|
||||
showLineNumbers={props.showLineNumbers}
|
||||
lineNumberStyle={{
|
||||
|
||||
@@ -44,6 +44,12 @@ const Features = () => {
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
case 'Vector':
|
||||
return await Telemetry.sendEvent(
|
||||
gaEvents['www_hp_subhero_products_vector'],
|
||||
telemetryProps,
|
||||
router
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +60,7 @@ const Features = () => {
|
||||
<div key={name} className="mb-10 space-y-4 md:mb-0">
|
||||
<div className="flex items-center">
|
||||
<ProductIcon icon={icon} />
|
||||
<dt className="text-scale-1200 ml-4 flex flex-row xl:flex-col">{name}</dt>
|
||||
<dt className="text-scale-1200 ml-3 flex flex-row xl:flex-col">{name}</dt>
|
||||
</div>
|
||||
|
||||
<p className="p">{description}</p>
|
||||
@@ -78,7 +84,7 @@ const Features = () => {
|
||||
return (
|
||||
<SectionContainer className="space-y-16 pb-0">
|
||||
<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">
|
||||
<dl className="grid grid-cols-1 gap-y-4 sm:grid-cols-2 md:grid-cols-2 md:gap-16 xl:grid-cols-6 lg:gap-x-8">
|
||||
{IconSections}
|
||||
</dl>
|
||||
</SectionContainer>
|
||||
|
||||
@@ -9,14 +9,18 @@ import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wo
|
||||
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Footer = () => {
|
||||
interface Props {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Footer = (props: Props) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const { pathname } = useRouter()
|
||||
const isLaunchWeekPage = pathname.includes('launch-week')
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="border-scale-500 dark:border-scale-600 border-t"
|
||||
className={['border-scale-500 dark:border-scale-600 border-t', props.className].join(' ')}
|
||||
aria-labelledby="footerHeading"
|
||||
>
|
||||
<h2 id="footerHeading" className="sr-only">
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import React, { PropsWithChildren, useEffect, useRef } from 'react'
|
||||
|
||||
interface Props {
|
||||
outerClassName?: string
|
||||
innerClassName?: string
|
||||
hasActiveOnHover?: boolean
|
||||
hasInnerShimmer?: boolean
|
||||
shimmerFromColor?: string
|
||||
shimmerToColor?: string
|
||||
}
|
||||
|
||||
const InteractiveShimmerCard = ({
|
||||
outerClassName,
|
||||
innerClassName,
|
||||
hasActiveOnHover = false,
|
||||
hasInnerShimmer = false,
|
||||
shimmerFromColor,
|
||||
shimmerToColor,
|
||||
children,
|
||||
}: PropsWithChildren<{ outerClassName?: string; innerClassName?: string }>) => {
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const outerRef = useRef(null)
|
||||
const innerRef = useRef(null)
|
||||
|
||||
@@ -19,16 +32,20 @@ const InteractiveShimmerCard = ({
|
||||
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), `
|
||||
: ''
|
||||
const activeGlow =
|
||||
hasActiveOnHover && 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))`
|
||||
${activeGlow}radial-gradient(30rem circle at ${x}px ${y}px, ${
|
||||
shimmerFromColor ?? 'var(--colors-scale8)'
|
||||
}, ${shimmerToColor ?? 'var(--colors-scale3)'})`
|
||||
|
||||
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)`
|
||||
: ''
|
||||
innerElement.style.backgroundImage =
|
||||
hasInnerShimmer && 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(() => {
|
||||
@@ -44,13 +61,13 @@ const InteractiveShimmerCard = ({
|
||||
<div
|
||||
ref={outerRef}
|
||||
className={[
|
||||
'relative rounded-xl bg-slate-400 from-slate-800 to-slate-800 p-px transition-all shadow-md',
|
||||
'relative rounded-xl bg-scale-400 from-scale-800 to-scale-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',
|
||||
'relative h-full rounded-xl bg-scale-200 dark:bg-scale-300 overflow-hidden transition-all text-scale-1100',
|
||||
innerClassName,
|
||||
].join(' ')}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props)
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
|
||||
<div className="flex ticket-brick-swiper">
|
||||
<div className="flex swiper-transition-linear">
|
||||
<Swiper
|
||||
onSwiper={(swiper) => setSwiperInstance(swiper)}
|
||||
centeredSlides={true}
|
||||
|
||||
@@ -5,19 +5,27 @@ import Footer from 'components/Footer/index'
|
||||
type Props = {
|
||||
hideHeader?: boolean
|
||||
hideFooter?: boolean
|
||||
className?: string
|
||||
footerClassName?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const DefaultLayout = (props: Props) => {
|
||||
const { hideHeader = false, hideFooter = false, children } = props
|
||||
const {
|
||||
hideHeader = false,
|
||||
hideFooter = false,
|
||||
className = '',
|
||||
footerClassName = '',
|
||||
children,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideHeader && <Nav />}
|
||||
<div className="min-h-screen">
|
||||
<main>{children}</main>
|
||||
<main className={className}>{children}</main>
|
||||
</div>
|
||||
{!hideFooter && <Footer />}
|
||||
{!hideFooter && <Footer className={footerClassName} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
MotionValue,
|
||||
motion,
|
||||
useInView,
|
||||
useMotionValue,
|
||||
useSpring,
|
||||
useTransform,
|
||||
} from 'framer-motion'
|
||||
import { useBreakpoint } from 'common'
|
||||
import { DEFAULT_TRANSITION } from '~/lib/animations'
|
||||
import { Products } from './Sections/ProductsCta'
|
||||
import { PRODUCT_NAMES, PRODUCT_SHORTNAMES } from '~/lib/constants'
|
||||
|
||||
function MagnifiedProducts({ currentProduct }: { currentProduct?: Products }) {
|
||||
let mouseX = useMotionValue(Infinity)
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
onMouseMove={(e) => mouseX.set(e.pageX)}
|
||||
onMouseLeave={() => mouseX.set(Infinity)}
|
||||
className="mx-auto w-full max-w-md grid grid-cols-3 md:flex items-center justify-center gap-y-8 md:gap-4 px-4"
|
||||
>
|
||||
{Object.entries(products).map(([key, product], i) => (
|
||||
<Product
|
||||
mouseX={mouseX}
|
||||
product={product}
|
||||
isCurrentProduct={product.shortname === currentProduct}
|
||||
index={i}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
function Product({
|
||||
mouseX,
|
||||
product,
|
||||
index,
|
||||
isCurrentProduct,
|
||||
}: {
|
||||
mouseX: MotionValue
|
||||
product: any
|
||||
index: number
|
||||
isCurrentProduct: boolean
|
||||
}) {
|
||||
let ref = useRef<HTMLDivElement>(null)
|
||||
const isInView = useInView(ref, { margin: '-25%', once: true })
|
||||
const isMobile = useBreakpoint(768)
|
||||
|
||||
let distance = useTransform(mouseX, (val) => {
|
||||
let bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }
|
||||
|
||||
return val - bounds.x - bounds.width / 2
|
||||
})
|
||||
|
||||
let widthSync = useTransform(distance, [-150, 0, 150], [75, 110, 75])
|
||||
let width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 })
|
||||
|
||||
const xDelta = 91
|
||||
const initial = {
|
||||
x: isMobile ? 0 : index * -xDelta + 225,
|
||||
}
|
||||
const animate = {
|
||||
x: 0,
|
||||
transition: { ...DEFAULT_TRANSITION, delay: 0.5 },
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={[
|
||||
'relative mx-auto md:w-[150px] bg-transparent group',
|
||||
isCurrentProduct ? 'z-10' : 'z-0',
|
||||
].join(' ')}
|
||||
initial={initial}
|
||||
animate={isInView ? animate : initial}
|
||||
>
|
||||
<Link href={product.url}>
|
||||
<a className="flex w-full flex-col items-center text-center">
|
||||
<motion.div
|
||||
style={isMobile ? (undefined as any) : { width, willChange: 'width' }}
|
||||
className="relative w-[50px] aspect-square will-change-transform"
|
||||
>
|
||||
<Image
|
||||
src={product.icon}
|
||||
priority
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
lazyBoundary="100px"
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="text-brand-1100 dark:text-brand-900 flex justify-center relative opacity-70 md:absolute md:bottom-0 md:opacity-0 group-hover:opacity-100 transition-opacity md:translate-y-8 md:-left-20 md:md:-right-20 font-mono uppercase text-center text-xs mt-2">
|
||||
<span>{product.name}</span>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
const products = {
|
||||
database: {
|
||||
shortname: PRODUCT_SHORTNAMES.DATABASE,
|
||||
name: PRODUCT_NAMES.DATABASE,
|
||||
icon: '/images/product/database/database-v2.png',
|
||||
description:
|
||||
"Every project is a full Postgres database, the world's most trusted relational database.",
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/database',
|
||||
},
|
||||
authentication: {
|
||||
shortname: PRODUCT_SHORTNAMES.AUTH,
|
||||
name: PRODUCT_NAMES.AUTH,
|
||||
icon: '/images/product/auth/auth-v2.png',
|
||||
description: 'Add user sign ups and logins, securing your data with Row Level Security.',
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/auth',
|
||||
},
|
||||
storage: {
|
||||
shortname: PRODUCT_SHORTNAMES.STORAGE,
|
||||
name: PRODUCT_NAMES.STORAGE,
|
||||
icon: '/images/product/storage/storage-v2.png',
|
||||
description: 'Store, organize, and serve large files. Any media, including videos and images.',
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/storage',
|
||||
},
|
||||
'edge-functions': {
|
||||
shortname: PRODUCT_SHORTNAMES.FUNCTIONS,
|
||||
name: PRODUCT_NAMES.FUNCTIONS,
|
||||
icon: '/images/product/functions/functions-v2.png',
|
||||
description: 'Write custom code without deploying or scaling servers.',
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/edge-functions',
|
||||
},
|
||||
realtime: {
|
||||
shortname: PRODUCT_SHORTNAMES.REALTIME,
|
||||
name: PRODUCT_NAMES.REALTIME,
|
||||
icon: '/images/product/realtime/realtime-v2.png',
|
||||
description:
|
||||
'Create multiplayer experiences by sharing, broadcasting, and listening to changes from other clients or the Database.',
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/realtime',
|
||||
},
|
||||
vector: {
|
||||
shortname: PRODUCT_SHORTNAMES.VECTOR,
|
||||
name: PRODUCT_NAMES.VECTOR,
|
||||
icon: '/images/product/vector/vector-v2.png',
|
||||
description: 'Integrate your favorite ML-models to store, index and search vector embeddings.',
|
||||
description_short: '',
|
||||
label: '',
|
||||
url: '/vector',
|
||||
},
|
||||
}
|
||||
|
||||
export default MagnifiedProducts
|
||||
@@ -73,7 +73,7 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
{i === 0 && (
|
||||
<tr className="">
|
||||
<td className="pb-1 bg-scale-700 px-3 py-1 -mr-1 border-l-4 border-scale-700">
|
||||
<span className="">Included in Free and Pro tiers</span>
|
||||
<span className="">Included in Free and Pro plan</span>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -88,9 +88,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
<td key={column.key} className="p-3">
|
||||
{column.key === 'dedicated' ? (
|
||||
column.value ? (
|
||||
<IconPricingIncludedCheck tier="Pro tier" />
|
||||
<IconPricingIncludedCheck tier="Pro plan" />
|
||||
) : (
|
||||
<IconPricingMinus tier="Free tier" />
|
||||
<IconPricingMinus tier="Free plan" />
|
||||
)
|
||||
) : (
|
||||
column.value
|
||||
@@ -124,9 +124,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
>
|
||||
{column.key === 'dedicated' ? (
|
||||
column.value ? (
|
||||
<IconPricingIncludedCheck tier="Pro tier" />
|
||||
<IconPricingIncludedCheck tier="Pro plan" />
|
||||
) : (
|
||||
<IconPricingMinus tier="Free tier" />
|
||||
<IconPricingMinus tier="Free plan" />
|
||||
)
|
||||
) : (
|
||||
column.value
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
image: string | ReactNode
|
||||
}
|
||||
|
||||
const CenteredTitleImage = ({ title, image }: Props) => {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div className="flex flex-col items-center text-center gap-8">
|
||||
<h2 className="heading-gradient text-2xl sm:text-3xl xl:text-4xl">{title}</h2>
|
||||
<div className="w-full max-w-5xl aspect-[2/1] md:aspect-[3/1] flex justify-center items-center mx-auto">
|
||||
{image}
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default CenteredTitleImage
|
||||
@@ -0,0 +1,220 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import { useWindowSize } from 'react-use'
|
||||
|
||||
import TweetCard from '~/components/TweetCard'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
import SwiperCore, { Autoplay } from 'swiper'
|
||||
|
||||
import 'swiper/swiper.min.css'
|
||||
import SectionContainer from '../Layouts/SectionContainer'
|
||||
import { useBreakpoint } from 'common'
|
||||
|
||||
SwiperCore.use([Autoplay])
|
||||
|
||||
interface CardInterface {
|
||||
type: 'twitter' | 'customer-story'
|
||||
avatar?: string
|
||||
author: string
|
||||
role?: string
|
||||
quote: string | React.ReactNode
|
||||
abstract?: string
|
||||
image?: string
|
||||
url?: string
|
||||
logo?: string
|
||||
customer?: string
|
||||
}
|
||||
|
||||
const vectorImagesDir = '/images/product/vector/community/'
|
||||
|
||||
const cards: CardInterface[] = [
|
||||
{
|
||||
type: 'twitter',
|
||||
avatar: '',
|
||||
author: 'Yasser',
|
||||
quote:
|
||||
'@kiwicopple @supabase @PostgreSQL @OpenAI Adding vector embeddings support to @Supabase is awesome. Glad I built https://t.co/jnCYOLa4iK on supabase.',
|
||||
},
|
||||
{
|
||||
type: 'customer-story',
|
||||
avatar: '',
|
||||
customer: 'Markprompt',
|
||||
author: 'Michael Fester',
|
||||
role: 'Co-Founder, Markprompt',
|
||||
quote:
|
||||
'We decided to use Supabase over other specialized vector databases because it enabled us to be GDPR compliant from day one with little effort.',
|
||||
image: vectorImagesDir + 'supabase+mendable.svg',
|
||||
abstract: 'Markprompt and Supabase - GDPR-Compliant AI Chatbots for Docs and Websites.',
|
||||
url: '/customers/markprompt',
|
||||
},
|
||||
{
|
||||
type: 'customer-story',
|
||||
avatar: '',
|
||||
customer: 'Mendable',
|
||||
author: 'Caleb Peffer',
|
||||
role: 'CEO, Mendable',
|
||||
quote:
|
||||
'We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re great, but if you need to store a bunch of metadata that becomes a huge pain.',
|
||||
image: vectorImagesDir + 'supabase+markprompt.svg',
|
||||
abstract: 'Mendable switches from Pinecone to Supabase for PostgreSQL vector embeddings.',
|
||||
url: '/customers/mendable',
|
||||
},
|
||||
{
|
||||
type: 'twitter',
|
||||
avatar: '',
|
||||
author: 'Batuhan',
|
||||
quote: (
|
||||
<>
|
||||
To create long-term memory for your ChatGPT application you can use @supabase vector
|
||||
database.
|
||||
<br />
|
||||
You can fix two common problems with this method:
|
||||
<br />
|
||||
Global Memory Token Size Limit Handling
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const CommunitySlider = () => {
|
||||
const ref = useRef<any>(null)
|
||||
const isSm = useBreakpoint(768)
|
||||
const { basePath } = useRouter()
|
||||
const [swiperInstance, setSwiperInstance] = useState<SwiperCore | any | null>(null)
|
||||
const { width } = useWindowSize()
|
||||
|
||||
useEffect(() => {
|
||||
// trigger autoplay if viewport resize
|
||||
if (swiperInstance) swiperInstance?.autoplay?.start()
|
||||
}, [width])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !swiperInstance?.autoplay) return
|
||||
|
||||
ref.current?.addEventListener('mouseover', () => swiperInstance?.autoplay?.stop())
|
||||
ref.current?.addEventListener('mouseleave', () => swiperInstance?.autoplay?.start())
|
||||
|
||||
return () => {
|
||||
ref.current?.removeEventListener('mouseover', () => swiperInstance?.autoplay?.stop())
|
||||
ref.current?.removeEventListener('mouseleave', () => swiperInstance?.autoplay?.start())
|
||||
}
|
||||
}, [ref.current, swiperInstance?.autoplay])
|
||||
|
||||
const Card = (card: CardInterface) => (
|
||||
<div className="dark:bg-scale-300 hover:border-scale-600 hover:dark:border-scale-700 border-scale-300 dark:border-scale-400 rounded-2xl border bg-white p-6 drop-shadow-sm flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 overflow-hidden rounded-full border dark:border-gray-600">
|
||||
<Image src={card.avatar!} layout="responsive" width="64" height="64" alt={card.author} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-scale-1200 font-medium">{card.author}</p>
|
||||
<p className="text-scale-900 text-sm">{card.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-scale-900 mt-2 text-base">{card.quote}</p>
|
||||
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<div className="relative h-10">
|
||||
<Image
|
||||
src={card.image!}
|
||||
alt={`Supabase + ${card.customer}`}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
objectPosition="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-scale-900 my-3 text-base">{card.abstract}</p>
|
||||
|
||||
{card.url && (
|
||||
<div className="text-brand-900 block cursor-pointer text-sm">Read Customer Story</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="py-16 sm:py-18 md:py-24 bg-scale-100">
|
||||
<div className="relative h-[300px] w-screen mx-auto md:w-full -mb-32 z-0">
|
||||
<Image
|
||||
src="/images/product/vector/community/vector-community.svg"
|
||||
alt="vector graphic"
|
||||
layout="fill"
|
||||
objectFit={isSm ? 'cover' : 'contain'}
|
||||
/>
|
||||
</div>
|
||||
<SectionContainer className="!py-0">
|
||||
<div className="col-span-12 text-center relative z-10">
|
||||
<h3 className="text-3xl md:text-4xl heading-gradient">
|
||||
Join a growing <br />
|
||||
community of users
|
||||
</h3>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
|
||||
<div ref={ref} className="relative mx-auto pt-16 pb-0 swiper-transition-linear">
|
||||
<Swiper
|
||||
onSwiper={(swiper) => setSwiperInstance(swiper)}
|
||||
centeredSlides={true}
|
||||
spaceBetween={30}
|
||||
slidesPerView={3}
|
||||
speed={13000}
|
||||
loop={true}
|
||||
watchOverflow
|
||||
threshold={2}
|
||||
updateOnWindowResize
|
||||
allowTouchMove={false}
|
||||
autoplay={{
|
||||
delay: 0,
|
||||
disableOnInteraction: true,
|
||||
reverseDirection: false,
|
||||
}}
|
||||
breakpoints={{
|
||||
320: {
|
||||
slidesPerView: 1.5,
|
||||
spaceBetween: 10,
|
||||
},
|
||||
720: {
|
||||
slidesPerView: 2.5,
|
||||
spaceBetween: 20,
|
||||
},
|
||||
1280: {
|
||||
slidesPerView: 4,
|
||||
spaceBetween: 30,
|
||||
},
|
||||
1700: {
|
||||
slidesPerView: 5,
|
||||
spaceBetween: 30,
|
||||
},
|
||||
2100: {
|
||||
slidesPerView: 6,
|
||||
spaceBetween: 30,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{cards.map((card, i) => (
|
||||
<SwiperSlide key={card.author}>
|
||||
{card.type === 'twitter' ? (
|
||||
<TweetCard
|
||||
handle={`@${card.author}`}
|
||||
quote={card.quote}
|
||||
img_url={`${basePath}${card.avatar}`}
|
||||
/>
|
||||
) : (
|
||||
<Link href={`${basePath}${card.url}`}>
|
||||
<a>
|
||||
<Card {...card} />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommunitySlider
|
||||
@@ -0,0 +1,115 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import { IconArrowUpRight } from 'ui'
|
||||
import { LazyMotion, domAnimation, m, useInView } from 'framer-motion'
|
||||
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import CustomersVisual from '~/components/products/vector/CustomersVisual'
|
||||
import { useRef } from 'react'
|
||||
import { INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
|
||||
|
||||
interface Customer {
|
||||
type?: string
|
||||
avatar?: string
|
||||
author?: string
|
||||
role?: string
|
||||
quote?: string | React.ReactNode
|
||||
abstract?: string
|
||||
image?: string
|
||||
url?: string
|
||||
logo?: string
|
||||
customer?: string
|
||||
}
|
||||
|
||||
interface Card {
|
||||
customer: any
|
||||
index: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string | React.ReactNode
|
||||
customers: Customer[]
|
||||
secondaryLinks: Customer[]
|
||||
}
|
||||
|
||||
const CustomerQuotesSection = (props: Props) => {
|
||||
const { basePath } = useRouter()
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { margin: '-25%', once: true })
|
||||
|
||||
const Card = ({ customer, index }: Card) => {
|
||||
const { isDarkMode } = useTheme()
|
||||
const logo = `/images/customers/logos/${!isDarkMode ? '' : 'light/'}${customer.customer}.png`
|
||||
|
||||
const initial = INITIAL_BOTTOM
|
||||
const animate = getAnimation({ delay: index * 0.1 })
|
||||
|
||||
return (
|
||||
<m.div
|
||||
initial={initial}
|
||||
animate={isInView ? animate : initial}
|
||||
className="bg-scale-100 dark:bg-scale-200 hover:border-scale-600 hover:dark:border-scale-700 border-scale-300 dark:border-scale-400 rounded-2xl border p-6 drop-shadow-sm flex flex-col justify-between"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="h-24 w-full flex items-center justify-center pb-3">
|
||||
<div className="relative w-full flex items-center justify-center h-10">
|
||||
<Image
|
||||
src={logo}
|
||||
alt={`Supabase + ${customer.customer}`}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
objectPosition="center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t pt-4">
|
||||
<blockquote className="text-scale-1100 text-base">{customer.quote}</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-scale-900 mt-4">
|
||||
{customer.author}, {customer.role}
|
||||
</p>
|
||||
{customer.url && (
|
||||
<div className="text-brand-900 border-t mt-4 pt-4 cursor-pointer text-sm flex items-center justify-between">
|
||||
<span>Read Customer Story</span>
|
||||
<IconArrowUpRight />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="py-16 sm:py-18 md:py-24 overflow-hidden">
|
||||
<div className="relative h-[300px] w-[300vw] left-[-98vw] md:left-0 mx-auto md:w-full -mb-32 z-0">
|
||||
<CustomersVisual />
|
||||
</div>
|
||||
<SectionContainer className="!py-0">
|
||||
<div className="col-span-12 text-center relative z-10">
|
||||
<h3 className="text-3xl md:text-4xl heading-gradient">{props.title}</h3>
|
||||
</div>
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative mx-auto max-w-4xl grid grid-cols-1 lg:grid-cols-2 gap-4 xl:gap-8 pt-16 pb-0"
|
||||
>
|
||||
{props.customers.map((customer, i: number) => (
|
||||
<Link href={`${basePath}${customer.url}`} key={customer.customer}>
|
||||
<a className="h-full flex">
|
||||
<Card customer={customer} index={i} />
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</div>
|
||||
</LazyMotion>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerQuotesSection
|
||||
@@ -0,0 +1,30 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Button } from 'ui'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
|
||||
const EnterpriseCta = () => {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<div className="flex flex-col text-center gap-4 py-8 items-center justify-center">
|
||||
<h2 className="heading-gradient text-2xl sm:text-3xl xl:text-4xl">
|
||||
Supabase Vector for Enterprise
|
||||
</h2>
|
||||
<p className="mx-auto text-scale-900 w-full">
|
||||
Talk to one of our experts about scaling Supabase Vector{' '}
|
||||
<br className="hidden md:block" />
|
||||
and managing embeddings at scale.
|
||||
</p>
|
||||
<div className="w-full mt-4 flex items-center justify-center text-center gap-4">
|
||||
<Link href="https://forms.supabase.com/enterprise">
|
||||
<a>
|
||||
<Button size="medium">Fill out Enterprise Form</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnterpriseCta
|
||||
@@ -0,0 +1,107 @@
|
||||
import Link from 'next/link'
|
||||
import React, { useRef, ReactNode } from 'react'
|
||||
import { LazyMotion, domAnimation, m, useInView } from 'framer-motion'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import { INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
|
||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown'
|
||||
import { Button, IconArrowUpRight } from 'ui'
|
||||
|
||||
interface Feature {
|
||||
icon: string
|
||||
title: string
|
||||
text: string
|
||||
}
|
||||
interface Props {
|
||||
title: string | ReactNode
|
||||
paragraph: string
|
||||
cta?: {
|
||||
label?: string
|
||||
link: string
|
||||
}
|
||||
features: Feature[]
|
||||
}
|
||||
|
||||
const FeaturesSection = ({ title, paragraph, cta, features }: Props) => {
|
||||
const ref = useRef(null)
|
||||
const isInView = useInView(ref, { margin: '-25%', once: true })
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<SectionContainer>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 xl:gap-10 justify-between">
|
||||
<div className="col-span-full lg:col-span-4 gap-2 flex flex-col">
|
||||
<h2 className="text-2xl sm:text-3xl xl:text-4xl max-w-[280px] sm:max-w-xs xl:max-w-[360px] tracking-[-1px]">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-scale-900 mb-4">{paragraph}</p>
|
||||
{cta && (
|
||||
<Link href={cta.link}>
|
||||
<a>
|
||||
<Button type="default" size="small" icon={<IconArrowUpRight />}>
|
||||
{cta.label ?? 'Explore documentation'}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={ref}
|
||||
className="col-span-full lg:col-start-6 lg:col-span-7 space-y-10 lg:space-y-0 flex flex-col lg:grid lg:grid-cols-2 lg:gap-16"
|
||||
>
|
||||
{features.map((feature: Feature, i: number) => (
|
||||
<Feature feature={feature} index={i} isInView={isInView} key={feature.title} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
</LazyMotion>
|
||||
)
|
||||
}
|
||||
|
||||
const Feature = ({
|
||||
feature,
|
||||
index,
|
||||
isInView,
|
||||
}: {
|
||||
feature: Feature
|
||||
index: number
|
||||
isInView: boolean
|
||||
}) => {
|
||||
const initial = INITIAL_BOTTOM
|
||||
const animate = getAnimation({ delay: index * 0.1 })
|
||||
|
||||
return (
|
||||
<m.div
|
||||
className="h-full flex items-start space-x-3 w-full"
|
||||
initial={initial}
|
||||
animate={isInView ? animate : initial}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="relative w-full h-6 flex items-center mx-auto">
|
||||
<svg
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d={feature.icon}
|
||||
stroke="var(--colors-scale12)"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm lg:text-base">
|
||||
<h2 className="text-base">{feature.title}</h2>
|
||||
<ReactMarkdown className="prose pt-1 text-sm text-scale-900">{feature.text}</ReactMarkdown>
|
||||
</div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeaturesSection
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { LazyMotion, domAnimation, m, useInView } from 'framer-motion'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import InteractiveShimmerCard from '~/components/InteractiveShimmerCard'
|
||||
import { INITIAL_BOTTOM, getAnimation } from '~/lib/animations'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Highlight {
|
||||
image?: React.ReactNode
|
||||
svg?: React.ReactNode
|
||||
title: string
|
||||
paragraph: string | React.ReactNode
|
||||
}
|
||||
|
||||
const HighlightCards = ({ highlights }: { highlights: Highlight[] }) => {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{highlights.map((highlight, i) => (
|
||||
<HighlightCard highlight={highlight} index={i} key={highlight.title} />
|
||||
))}
|
||||
</div>
|
||||
</LazyMotion>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const HighlightCard = ({ highlight, index }: { highlight: Highlight; index: number }) => {
|
||||
const ref = useRef(null)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const isInView = useInView(ref, { once: true })
|
||||
|
||||
const initial = INITIAL_BOTTOM
|
||||
const animate = getAnimation({ delay: 0.4 + index * 0.1 })
|
||||
|
||||
const Img: any = highlight.image
|
||||
|
||||
return (
|
||||
<m.div
|
||||
ref={ref}
|
||||
initial={initial}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
animate={isInView ? animate : initial}
|
||||
>
|
||||
<InteractiveShimmerCard innerClassName="flex flex-col !bg-scale-100">
|
||||
<div className="relative w-full aspect-[1.35/1] mb-4">
|
||||
<div
|
||||
className="absolute inset-0 w-full h-full z-10"
|
||||
style={{
|
||||
background: `radial-gradient(100% 50% at 50% 50%, transparent, var(--colors-scale1))`,
|
||||
}}
|
||||
/>
|
||||
{highlight.image && <Img isHovered={isHovered} />}
|
||||
{highlight.svg && highlight.svg}
|
||||
</div>
|
||||
<div className="p-8">
|
||||
<h3 className="text-lg text-scale-1200 mb-2">{highlight.title}</h3>
|
||||
<p className="text-scale-900">{highlight.paragraph}</p>
|
||||
</div>
|
||||
</InteractiveShimmerCard>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HighlightCards
|
||||