Skill: leonaaardob/lb-supabase-skill (version 0.1.0)

--- name: supabase description: Complete Supabase platform documentation. Use when working with Supabase - covers authentication (email, OAuth, magic links, phone, SSO), database (PostgreSQL, RLS, migrations), storage (file uploads, CDN), edge functions, realtime subscriptions, AI/embeddings, cron jobs, queues, and platform management. Includes framework integrations (Next.js, React, SvelteKit, etc.). --- ## ⚠️ Security Notice This skill contains Supabase documentation with command examples. Commands are **for reference only** and should NEVER be auto-executed by agents without explicit user approval. # Supabase Documentation Complete Supabase platform documentation embedded in markdown. Read from `references/` to answer questions about authentication, database, storage, edge functions, and platform features. ## Documentation Structure All documentation is in `references/guides/` organized by product: ### Getting Started ([REDACTED_SECRET]) - Quickstart guides - Architecture overview - Framework integrations - Database fundamentals ### Authentication (`references/guides/auth/`) Complete auth system documentation: - Email & password auth - OAuth providers (Google, GitHub, etc.) - Magic links - Phone auth (SMS, WhatsApp) - SSO & SAML - Multi-factor authentication (MFA) - Row Level Security (RLS) - User management - Server-side auth - Redirect URLs - Session management ### Database (`references/guides/database/`) PostgreSQL database features: - Tables and columns - Relationships and foreign keys - Functions and triggers - Extensions - Full-text search - Postgres roles - Replication - Connection pooling - Webhooks - Migrations ### Storage (`references/guides/storage/`) File storage and CDN: - Upload files - Download files - Delete files - List files - Transform images - CDN and caching - Access control with RLS - Resumable uploads ### Edge Functions (`references/guides/functions/`) Serverless functions: - Getting started - Deploy functions - Environment variables - Secrets management - Database access - Auth integration - Logging and monitoring - Cold starts optimization ### Realtime (`references/guides/realtime/`) Realtime subscriptions: - Postgres changes (inserts, updates, deletes) - Broadcast messages - Presence tracking - Authorization ### AI & Embeddings (`references/guides/ai/`) AI and vector features: - Embeddings and vector search - pgvector extension - Similarity search - RAG patterns - AI integrations (OpenAI, etc.) ### Platform (`references/guides/platform/`) Platform management: - Organizations - Projects - Billing - Logs and monitoring - Performance tuning - Backups - Custom domains - SSL certificates ### Self-Hosting (`references/guides/self-hosting/`) Self-hosted Supabase: - Docker setup - Configuration - Monitoring - Backups and restore ### Cron Jobs (`references/guides/cron/`) Scheduled tasks with pg_cron ### Queues (`references/guides/queues/`) Background job queues with pgmq ### Integrations (`references/guides/integrations/`) Third-party integrations and tools ### Local Development ([REDACTED_SECRET]) - CLI setup - Local studio - Database migrations - Testing - CI/CD ### Troubleshooting (`references/troubleshooting/`) Common issues and solutions ### Error Codes (`references/errorCodes/`) API and database error reference ## Quick Reference ### Common Tasks | Task | Directory to Check | |------|-------------------| | Setup Supabase | `guides/getting-started/` | | Email auth | `guides/auth/` | | OAuth providers | `guides/auth/` | | Database schema | `guides/database/` | | RLS policies | `guides/auth/` + `guides/database/` | | File uploads | `guides/storage/` | | Edge functions | `guides/functions/` | | Realtime subscriptions | `guides/realtime/` | | Vector search | `guides/ai/` | | Migrations | `guides/database/` + `guides/local-development/` | | Framework integration | `guides/getting-started/` | | Self-hosting | `guides/self-hosting/` | ### Framework Integrations Supabase works with: - Next.js (App Router, Pages Router, Server Components) - React (Create React App, Vite) - SvelteKit - Nuxt - Vue - Angular - Flutter - React Native - And more... ### When to Use This Skill - Setting up Supabase authentication - Database schema design with PostgreSQL - Row Level Security (RLS) policies - File storage and CDN - Edge functions deployment - Realtime subscriptions - Vector search and AI features - Migration from other platforms - Self-hosting Supabase - Performance optimization - Troubleshooting errors ### How to Navigate 1. **Start with `guides/getting-started/`** for setup 2. **For auth:** Browse `guides/auth/` 3. **For database:** Check `guides/database/` 4. **For storage:** See `guides/storage/` 5. **For functions:** Use `guides/functions/` 6. **For realtime:** Check `guides/realtime/` 7. **For AI:** See `guides/ai/` 8. **For errors:** Check `troubleshooting/` and `errorCodes/` All files are `.mdx` (Markdown + JSX) but readable as plain markdown.

path: README.md
content: # Supabase Platform Documentation Skill

Complete Supabase platform documentation packaged as an OpenClaw AgentSkill.

## Contents

- **Authentication** (email, OAuth, magic links, phone, SSO, MFA)
- **Database** (PostgreSQL, RLS, migrations, triggers, functions)
- **Storage** (file uploads, CDN, image transformations)
- **Edge Functions** (serverless, Deno runtime)
- **Realtime** (subscriptions, broadcast, presence)
- **AI & Embeddings** (pgvector, similarity search, RAG)
- **Platform** (organizations, billing, monitoring)
- **Self-Hosting** (Docker, configuration)
- **Framework Integrations** (Next.js, React, SvelteKit, etc.)

## Structure

```
references/
├── guides/
│   ├── getting-started/    # Quickstarts
│   ├── auth/               # Authentication
│   ├── database/           # PostgreSQL
│   ├── storage/            # File storage
│   ├── functions/          # Edge functions
│   ├── realtime/           # Realtime subscriptions
│   ├── ai/                 # AI & embeddings
│   ├── platform/           # Platform management
│   ├── self-hosting/       # Self-hosted Supabase
│   ├── cron/               # Scheduled jobs
│   ├── queues/             # Background jobs
│   ├── integrations/       # Third-party tools
│   └── local-development/  # CLI & local setup
├── troubleshooting/        # Common issues
└── errorCodes/             # Error reference
```

## Installation

Via ClawHub:
```bash
clawhub install lb-supabase-skill
```

Or manually: Download and extract into your OpenClaw workspace `skills/` folder.

## Usage

This skill triggers automatically when you ask questions about Supabase authentication, database, storage, edge functions, realtime, AI features, or platform management.

## Covered Topics

### Authentication
- Email & password auth
- OAuth providers (Google, GitHub, Apple, Discord, etc.)
- Magic links
- Phone auth (SMS, WhatsApp)
- SSO & SAML
- Multi-factor authentication (MFA/2FA)
- Row Level Security (RLS)
- Server-side auth
- Session management

### Database
- PostgreSQL setup
- Schema design
- Relationships and foreign keys
- Functions and triggers
- Extensions (pgvector, postgis, etc.)
- Full-text search
- Migrations
- Connection pooling
- Webhooks

### Storage
- File uploads
- Download and delete files
- Image transformations
- CDN configuration
- Access control with RLS
- Resumable uploads

### Edge Functions
- Serverless Deno functions
- Database access
- Auth integration
- Environment variables
- Secrets management
- Logging and monitoring

### Realtime
- Postgres changes subscriptions
- Broadcast messages
- Presence tracking
- Authorization

### AI & Vector Search
- pgvector extension
- Embeddings and similarity search
- RAG patterns
- OpenAI integration

### Platform
- Organizations and projects
- Billing and usage
- Logs and monitoring
- Performance tuning
- Backups and restore
- Custom domains

### Framework Integrations
- Next.js (App Router, Pages Router)
- React
- SvelteKit
- Nuxt
- Vue
- Angular
- Flutter
- React Native

## Source

Documentation extracted from [supabase/supabase](https://github.com/supabase/supabase) (latest version).

## License

Documentation content: Apache 2.0 (from Supabase project)


path: references/_partials/_fixtures/hyphenated-variables.mdx
content: This partial has {{ .my-var }}, {{ .another_var }}, and {{ .myVar123 }}.


path: references/_partials/_fixtures/multiple-variables.mdx
content: This partial has {{ .var1 }}, {{ .var2 }}, and {{ .var3 }}.


path: references/_partials/_fixtures/nested1.mdx
content: First level of nesting:

{/* prettier-ignore */}
<$Partial path='/_fixtures/nested2.mdx' />


path: references/_partials/_fixtures/nested2.mdx
content: Second level of nesting.


path: references/_partials/_fixtures/test.mdx
content: Here is some content to embed as a partial.

## A heading

Some more stuff, including:

- A list
- With two items

And some custom components:

<Admonition type="note">

Some stuff.

In two paragraphs.

</Admonition>

<Component />

An entire paragraph: It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way...


path: references/_partials/_fixtures/variables.mdx
content: Here is a partial that takes a {{ .var }}.


path: references/_partials/ai/quickstart_hf_deployment.mdx
content: ## Deployment

If you have your own infrastructure for deploying Python apps, you can continue to use `vecs` as described in this guide.

Alternatively if you would like to quickly deploy using Supabase, check out our guide on using the [Hugging Face Inference API](/docs/guides/ai/hugging-face) in Edge Functions using TypeScript.


path: references/_partials/api_rate_limits.mdx
content: ## Rate limits

Rate limits are applied to prevent abuse and ensure fair usage of the Management API. Rate limits are based on a per-user, per-scope model, meaning each user gets independent rate limits for each project and organization they interact with.

### Standard rate limit

| Limit        | Duration | Scope                              |
| ------------ | -------- | ---------------------------------- |
| 120 requests | 1 minute | Per user, per project/organization |

When you exceed this rate limit, all subsequent API calls will return a `429 Too Many Requests` response for the remainder of the minute. Once the time window expires, your request quota resets and you can make requests again.

### Rate limit scope

Rate limits are applied with per-user + per-scope isolation:

- **Project scope**: Rate limits apply independently to each project. Requests to one project do not count toward the limit of another project.
- **Organization scope**: Rate limits apply independently to each organization. Requests to one organization do not count toward the limit of another organization.

This means you can make 120 requests to Project A and 120 requests to Project B within the same minute without hitting rate limits, as they are tracked separately.

### Rate limit response headers

Every API response includes rate limit information in the following headers:

- `X-RateLimit-Limit` - The maximum number of requests allowed in the current time window
- `X-RateLimit-Remaining` - The number of requests remaining before you hit the rate limit
- `X-RateLimit-Reset` - The number of milliseconds remaining until your rate limit resets

You can use these headers to monitor your usage and implement proactive rate limit handling before receiving a 429 response.

### How rate limits are tracked

Your requests are identified and tracked using one of the following identifiers, in this order of priority:

1. **OAuth App ID** - If your request is authenticated via an OAuth application
2. **User ID** - If your request is authenticated with a personal access token
3. **IP Address** - If your request is unauthenticated (extracted from request headers)

Each identifier is combined with the scope (project or organization) to create a unique tracking key. This ensures that rate limits are isolated per user and per scope, preventing one project or organization from affecting another.

### Endpoint exceptions

Some endpoints have stricter rate limits than the standard 120 requests per minute to prevent abuse of resource-intensive operations:

| Endpoint                                                   | Limit       | Duration | Reason                                              |
| ---------------------------------------------------------- | ----------- | -------- | --------------------------------------------------- |
| `GET /v1/projects/:ref/endpoints/logs.all`                 | 30 requests | 1 minute | Analytics log queries are computationally expensive |
| `GET /v1/projects/:ref/endpoints/usage.api-counts`         | 30 requests | 1 minute | Analytics aggregation is computationally expensive  |
| `GET /v1/projects/:ref/endpoints/usage.api-requests-count` | 30 requests | 1 minute | Analytics aggregation is computationally expensive  |
| `GET /v1/projects/:ref/database/context`                   | 10 requests | 1 minute | Database context operations are resource-intensive  |
| `GET /v1/projects/:ref/database/context`                   | 1 request   | 1 second | Burst limit to prevent rapid successive requests    |
| `POST /v1/projects/:ref/config/custom-hostname/initialize` | 10 requests | 1 minute | These operations are expensive                      |
| `POST /v1/projects/:ref/config/custom-hostname/reverify`   | 10 requests | 1 minute | These operations are expensive                      |
| `DELETE /v1/projects/:ref/config/custom-hostname`          | 10 requests | 1 minute | These operations are expensive                      |
| `GET /v1/projects/:ref/config/vanity-subdomain`            | 10 requests | 1 minute | These operations are expensive                      |

**Note:** The `GET /v1/projects/:ref/database/context` endpoint has dual rate limiting. You can make up to 10 requests per minute, but also no more than 1 request per second to prevent burst traffic.

### Best practices

- **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit.
- **Implement exponential backoff** - When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (milliseconds) to determine exactly how long to wait.
- **Batch operations** - Where possible, combine multiple operations into fewer API calls to reduce your request count.
- **Be mindful of expensive endpoints** - Analytics, database context, and domain endpoints have stricter limits, so use them judiciously.


path: references/_partials/api_settings.mdx
content: ### Get API details

Now that you've created some database tables, you are ready to insert data using the auto-generated API.

To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).

<Admonition type="note" title="Changes to API keys">

Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys.

In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/):

- **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.
- **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.

</Admonition>

[Read the API keys docs](/docs/guides/api/api-keys) for a full explanation of all key types and their uses.


path: references/_partials/api_settings_steps.mdx
content: {/* TODO: How to completely consolidate partials? */}

You can also get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).

<Admonition type="note" title="Changes to API keys">

Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys.

In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/):

- **For legacy keys**, copy the `anon` key for client-side operations and the `service_role` key for server-side operations from the **Legacy API Keys** tab.
- **For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.

</Admonition>

[Read the API keys docs](/docs/guides/api/api-keys) for a full explanation of all key types and their uses.


path: references/_partials/auth_rate_limits.mdx
content: | Endpoint                                         | Path                                                           | Limited By               | Rate Limit                                                                                                                                                                                                                                                     |
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| All endpoints that send emails                   | `/auth/v1/signup` `/auth/v1/recover` `/auth/v1/user`[^1]       | Sum of combined requests | Defaults to 4 emails per hour as of 14th July 2023. As of 21 Oct 2023, this has been updated to <SharedData data="config">auth.rate_limits.email.inbuilt_smtp_per_hour</SharedData> emails per hour. You can only change this with your own custom SMTP setup. |
| All endpoints that send One-Time-Passwords (OTP) | `/auth/v1/otp`                                                 | Sum of combined requests | Defaults to <SharedData data="config">auth.rate_limits.otp.requests_per_hour</SharedData> OTPs per hour. Is customizable.                                                                                                                                      |
| Send OTPs or magic links                         | `/auth/v1/otp`                                                 | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.otp.period</SharedData> window before a new request is allowed to the same user. Is customizable.                                                                                                       |
| Signup confirmation request                      | `/auth/v1/signup`                                              | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.signup_confirmation.period</SharedData> window before a new request is allowed to the same user. Is customizable.                                                                                       |
| Password Reset Request                           | `/auth/v1/recover`                                             | Last request of the user | Defaults to <SharedData data="config">auth.rate_limits.password_reset.period</SharedData> window before a new request is allowed to the same user. Is customizable.                                                                                            |
| Verification requests                            | `/auth/v1/verify`                                              | IP Address               | <SharedData data="config">auth.rate_limits.verification.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.verification.requests_burst</SharedData> requests)                                      |
| Token refresh requests                           | `/auth/v1/token`                                               | IP Address               | <SharedData data="config">auth.rate_limits.token_refresh.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.token_refresh.requests_burst</SharedData> requests)                                    |
| Create or Verify an MFA challenge                | `/auth/v1/factors/:id/challenge` `/auth/v1/factors/:id/verify` | IP Address               | <SharedData data="config">auth.rate_limits.mfa.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.verification.mfa</SharedData> requests)                                                          |
| Anonymous sign-ins                               | `/auth/v1/signup`[^2]                                          | IP Address               | <SharedData data="config">auth.rate_limits.anonymous_signin.requests_per_hour</SharedData> requests per hour (with bursts up to <SharedData data="config">auth.rate_limits.anonymous_signin.requests_burst</SharedData> requests)                              |

[^1]: The rate limit is only applied on `/auth/v1/user` if this endpoint is called to update the user's email address.
[^2]: The rate limit is only applied on `/auth/v1/signup` if this endpoint is called without passing in an email or phone number in the request body.


path: references/_partials/billing/exceeding_usage_quotas.mdx
content: If you are on a paid plan and have [Spend Cap](/docs/guides/platform/cost-control#spend-cap) disabled or your organization is on Team Plan or above, you will pay for any overages.

When you are exceeding your quotas while being on a Free Plan or having [Spend Cap](/docs/guides/platform/cost-control#spend-cap) enabled, you will get a notification to your billing email address and put under a grace period. For more details, refer to our [Fair Use Policy](/docs/guides/platform/billing-faq#fair-use-policy).


path: references/_partials/billing/pricing/pricing_branching.mdx
content: There is no fixed fee for a Preview branch. You only pay for the usage it incurs. A branch running on the default Micro Compute size starts at <Price price="0.01344" /> per hour.


path: references/_partials/billing/pricing/pricing_custom_domains.mdx
content: ## Pricing

For a detailed breakdown of how charges are calculated, refer to [Manage Custom Domain usage](/docs/guides/platform/manage-your-usage/custom-domains).


path: references/_partials/billing/pricing/pricing_edge_functions.mdx
content: <Price price="2" /> per 1 million invocations. You are only charged for usage exceeding your subscription
plan's quota.

| Plan       | Quota     | Over-Usage                                    |
| ---------- | --------- | --------------------------------------------- |
| Free       | 500,000   | -                                             |
| Pro        | 2 million | <Price price="2" /> per 1 million invocations |
| Team       | 2 million | <Price price="2" /> per 1 million invocations |
| Enterprise | Custom    | Custom                                        |


path: references/_partials/billing/pricing/pricing_ipv4.mdx
content: ## Pricing

For a detailed breakdown of how charges are calculated, refer to [Manage IPv4 usage](/docs/guides/platform/manage-your-usage/ipv4).


path: references/_partials/billing/pricing/pricing_mau.mdx
content: <Price price="0.00325" /> per MAU. You are only charged for usage exceeding your subscription plan's
quota.


path: references/_partials/billing/pricing/pricing_mau_sso.mdx
content: ## Pricing

<Price price="0.015" /> per SSO MAU. You are only charged for usage exceeding your subscription plan's
quota.

For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active SSO Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-sso).


path: references/_partials/billing/pricing/pricing_mau_third_party.mdx
content: ## Pricing

<Price price="0.00325" /> per Third-Party MAU. You are only charged for usage exceeding your subscription
plan's quota.

For a detailed breakdown of how charges are calculated, refer to [Manage Monthly Active Third-Party Users usage](/docs/guides/platform/manage-your-usage/monthly-active-users-third-party).


path: references/_partials/billing/pricing/pricing_mfa_phone.mdx
content: ## Pricing

<Price price="0.1027" /> per hour (<Price price="75" /> per month) for the first project. <Price price="0.0137" /> per
hour (<Price price="10" /> per month) for every additional project.

| Plan       | Project 1 per month  | Project 2 per month  | Project 3 per month  |
| ---------- | -------------------- | -------------------- | -------------------- |
| Pro        | <Price price="75" /> | <Price price="10" /> | <Price price="10" /> |
| Team       | <Price price="75" /> | <Price price="10" /> | <Price price="10" /> |
| Enterprise | Custom               | Custom               | Custom               |

For a detailed breakdown of how charges are calculated, refer to [Manage Advanced MFA Phone usage](/docs/guides/platform/manage-your-usage/advanced-mfa-phone).


path: references/_partials/billing/pricing/pricing_pitr.mdx
content: ### Pricing

Pricing depends on the recovery retention period, which determines how many days back you can restore data to any chosen point of up to seconds in granularity.

| Recovery Retention Period in Days | Hourly Price USD        | Monthly Price USD     |
| --------------------------------- | ----------------------- | --------------------- |
| 7                                 | <Price price="0.137" /> | <Price price="100" /> |
| 14                                | <Price price="0.274" /> | <Price price="200" /> |
| 28                                | <Price price="0.55" />  | <Price price="400" /> |

For a detailed breakdown of how charges are calculated, refer to [Manage Point-in-Time Recovery usage](/docs/guides/platform/manage-your-usage/point-in-time-recovery).


path: references/_partials/billing/pricing/pricing_realtime_connections.mdx
content: <Price price="10" /> per 1,000 peak connections. You are only charged for usage exceeding your subscription
plan's quota.

| Plan       | Quota  | Over-Usage                                      |
| ---------- | ------ | ----------------------------------------------- |
| Free       | 200    | -                                               |
| Pro        | 500    | <Price price="10" /> per 1,000 peak connections |
| Team       | 500    | <Price price="10" /> per 1,000 peak connections |
| Enterprise | Custom | Custom                                          |


path: references/_partials/billing/pricing/pricing_realtime_messages.mdx
content: <Price price="2.50" /> per 1 million messages. You are only charged for usage exceeding your subscription
plan's quota.

| Plan       | Quota     | Over-Usage                                    |
| ---------- | --------- | --------------------------------------------- |
| Free       | 2 million | -                                             |
| Pro        | 5 million | <Price price="2.50" /> per 1 million messages |
| Team       | 5 million | <Price price="2.50" /> per 1 million messages |
| Enterprise | Custom    | Custom                                        |


path: references/_partials/billing/pricing/pricing_storage_image_transformations.mdx
content: <Price price="5" /> per 1,000 origin images. You are only charged for usage exceeding your subscription
plan's quota.

<Admonition type="note">

The count resets at the start of each billing cycle.

</Admonition>

| Plan       | Quota  | Over-Usage                                  |
| ---------- | ------ | ------------------------------------------- |
| Pro        | 100    | <Price price="5" /> per 1,000 origin images |
| Team       | 100    | <Price price="5" /> per 1,000 origin images |
| Enterprise | Custom | Custom                                      |

For a detailed breakdown of how charges are calculated, refer to [Manage Storage Image Transformations usage](/docs/guides/platform/manage-your-usage/storage-image-transformations).


path: references/_partials/billing/pricing/pricing_storage_size.mdx
content: <Price price="0.00002919" /> per GB-Hr (<Price price="0.021" /> per GB per month). You are only
charged for usage exceeding your subscription plan's quota.

| Plan       | Quota in GB | Over-Usage per GB       | Quota in GB-Hrs | Over-Usage per GB-Hr         |
| ---------- | ----------- | ----------------------- | --------------- | ---------------------------- |
| Free       | 1           | -                       | 744             | -                            |
| Pro        | 100         | <Price price="0.021" /> | 74,400          | <Price price="0.00002919" /> |
| Team       | 100         | <Price price="0.021" /> | 74,400          | <Price price="0.00002919" /> |
| Enterprise | Custom      | Custom                  | Custom          | Custom                       |


path: references/_partials/create_client_snippet.mdx
content: <Admonition type="tip">

Make sure you're using the right `supabase` client in the following code.

If you're not using Server-Side Rendering or cookie-based Auth, you can directly use the `createClient` from `@supabase/supabase-js`. If you're using Server-Side Rendering, see the [Server-Side Auth guide](/docs/guides/auth/server-side/creating-a-client) for instructions on creating your Supabase client.

</Admonition>


path: references/_partials/database_setup.mdx
content: ## 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 on the dashboard:

- [Database connection strings](/dashboard/project/_/settings/api?showConnect=true): Direct and Pooler connection details including the connection string and parameters.
- [Database password](/dashboard/project/_/database/settings): Reset database password here if you do not have it.
- [API credentials](/dashboard/project/_/settings/api): your serverless API URL and `anon` / `service_role` keys.


path: references/_partials/db_pre_request_warning.mdx
content: <Admonition type="caution">

The `pgrst.db_pre_request` configuration only works with the **Data API** (PostgREST). It does not work with Realtime, Storage, or other Supabase products.

If you're using `db_pre_request` to call a function (like `set_information()`) that sets up context or performs checks on every request, and you need similar behavior for other Supabase products, you must call the function directly in your Row Level Security (RLS) policies instead.

**Example:**

If you have a `db_pre_request` function that calls `set_information()` that returns `true` to set up context or perform checks, and you have an RLS policy like:

```sql
create policy "Individuals can view their own todos."
on todos for select
using ( (select auth.uid()) = user_id );
```

To achieve the same behavior with other Supabase products, you need to call the function directly in your RLS policy:

```sql
create policy "Individuals can view their own todos."
on todos for select
using ( set_information() AND (select auth.uid()) = user_id );
```

This ensures the function is called when evaluating RLS policies for all products, not just Data API requests.

**Performance consideration:**

Be aware that calling functions directly in RLS policies can impact database performance, as the function is evaluated for each row when the policy is checked. Consider optimizing your function or using caching strategies if performance becomes an issue.

</Admonition>


path: references/_partials/get_session_warning.mdx
content: <Admonition type="danger">

Note that `auth.getSession` reads the auth token and the unencoded session data from the local storage medium. It _doesn't_ send a request back to the Supabase Auth server unless the local session is expired.

You should **never** trust the unencoded session data if you're writing server code, since it could be tampered with by the sender. If you need verified, trustworthy user data, call `auth.getUser` instead, which always makes a request to the Auth server to fetch trusted data.

</Admonition>


path: references/_partials/kotlin_project_setup.mdx
content: ## Project setup

Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database.

### Create a project

1. [Create a new project](https://app.supabase.com) in the Supabase Dashboard.
1. Enter your project details.
1. Wait for the new database to launch.

### Set up the database schema

Now we are going to set up the database schema. You can just copy/paste the SQL from below and run it yourself.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="sql"
  queryGroup="database-method"
>
{/* <TabPanel id="dashboard" label="Dashboard">

1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
2. Click **Product Management**.
3. Click **Run**.

</TabPanel> */}
<TabPanel id="sql" label="SQL">

<$Partial path="product_management_sql_template.mdx" />

</TabPanel>
</Tabs>

<$Partial path="api_settings.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }}
/>

### Set up Google authentication

From the [Google Console](https://console.developers.google.com/apis/library), create a new project and add OAuth2 credentials.

![Create Google OAuth credentials](/docs/img/guides/kotlin/google-cloud-oauth-credentials-create.png)

In your [Supabase Auth settings](https://app.supabase.com/project/_/auth/providers) enable Google as a provider and set the required credentials as outlined in the [auth docs](/docs/guides/auth/social-login/auth-google).


path: references/_partials/mcp_postgres_config.mdx
content: <Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="mac"
  queryGroup="os"
>

  <TabPanel id="mac" label="macOS">

    ```json
    {
      "mcpServers": {
        "supabase": {
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-postgres", "<connection-string>"]
        }
      }
    }
    ```

    Replace `<connection-string>` with your connection string.

  </TabPanel>

  <TabPanel id="windows" label="Windows">

    ```json
    {
      "mcpServers": {
        "supabase": {
          "command": "cmd",
          "args": ["/c", "npx", "-y", "@modelcontextprotocol/server-postgres", "<connection-string>"]
        }
      }
    }
    ```

    Replace `<connection-string>` with your connection string.

    <Admonition type="note">

    Make sure that `node` and `npx` are available in your system `PATH`. Assuming `node` is installed, you can get the path by running:

    ```shell
    npm config get prefix
    ```

    Then add it to your system `PATH` by running:

    ```shell
    setx PATH "%PATH%;<path-to-dir>"
    ```

    Replacing `<path-to-dir>` with the path you got from the previous command.

    Finally restart {{ .app }} for the changes to take effect.

    </Admonition>

  </TabPanel>

  <TabPanel id="wsl" label="Windows (WSL)">

    ```json
    {
      "mcpServers": {
        "supabase": {
          "command": "wsl",
          "args": ["npx", "-y", "@modelcontextprotocol/server-postgres", "<connection-string>"]
        }
      }
    }
    ```

    Replace `<connection-string>` with your connection string.

    This assumes you have Windows Subsystem for Linux (WSL) enabled and `node`/`npx` are installed within the WSL environment.

  </TabPanel>

  <TabPanel id="linux" label="Linux">

    ```json
    {
      "mcpServers": {
        "supabase": {
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-postgres", "<connection-string>"]
        }
      }
    }
    ```

    Replace `<connection-string>` with your connection string.

  </TabPanel>

</Tabs>


path: references/_partials/metrics_access.mdx
content: <Accordion
type="default"
openBehaviour="multiple"
chevronAlign="right"
justified
size="medium"
className="rounded-lg border border-foreground/10 bg-surface-100 text-foreground-light [&>div]:space-y-4 p-4"
>

    <AccordionItem
    header={<span className="text-foreground font-medium">What you can do with the Metrics API</span>}
    id="how-do-i-check-when-a-user-went-through-mfa"
    className="border-0 px-2 py-4"
    >

    Every Supabase project exposes a metrics feed at `https://<project-ref>.supabase.co/customer/v1/privileged/metrics`. Replace `<project-ref>` with the identifier from your project URL or from the dashboard sidebar.

    1. Copy your project reference and confirm the base URL using the helper below.

    <ProjectConfigVariables variable="url" />

    2. Configure your collector to scrape once per minute. The endpoint already emits the full set of metrics on each request.
    3. Authenticate with HTTP Basic Auth:

    - **Username**: `service_role`
    - **Password**: a **Secret API key** (`sb_secret_...`). You can create/copy it in [**Project Settings → API Keys**](/dashboard/project/_/settings/api-keys). For more context, see [Understanding API keys](/docs/guides/api/api-keys).

    Testing locally is as simple as running `curl` with your Secret API key:

    ```bash
    curl <project-url>/customer/v1/privileged/metrics \
    --user 'service_role:sb_secret_...'
    ```

    You can provision long-lived automation tokens in two ways:

    - Create an account access token once at [**Account Settings > Access Tokens**](/dashboard/account/tokens) and reuse it wherever you configure observability tooling.
    - **Optional**: programmatically exchange an access token for project API keys via the [Management API](/docs/reference/api/management-projects-api-keys-retrieve).

    ```bash
    # (Optional) Exchange an account access token for project API keys
    export SUPABASE_ACCESS_TOKEN="your-access-token"
    export PROJECT_REF="your-project-ref"

    curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
    "https://api.supabase.com/v1/projects/$PROJECT_REF/api-keys?reveal=true"
    ```

    </AccordionItem>

</Accordion>


path: references/_partials/migration_warnings.mdx
content: <Admonition type="caution">

- If you're planning to migrate a database larger than 6 GB, we recommend [upgrading to at least a Large compute add-on](/docs/guides/platform/compute-add-ons). This will ensure you have the necessary resources to handle the migration efficiently.

- We strongly advise you to pre-provision the disk space you will need for your migration. On paid projects, you can do this by navigating to the [Compute and Disk Settings](/dashboard/project/_/settings/compute-and-disk) page. For more information on disk scaling and disk limits, check out our [disk settings](/docs/guides/platform/compute-and-disk#disk) documentation.

</Admonition>


path: references/_partials/oauth_pkce_flow.mdx
content: For a PKCE flow, for example in Server-Side Auth, you need an extra step to handle the code exchange. When calling `signInWithOAuth`, provide a `redirectTo` URL which points to a callback route. This redirect URL should be added to your [redirect allow list](/docs/guides/auth/redirect-urls).

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="client"
  queryGroup="environment"
>
<TabPanel id="client" label="Client">

In the browser, `signInWithOAuth` automatically redirects to the OAuth provider's authentication endpoint, which then redirects to your endpoint.

```js
import { createClient, type Provider } from '@supabase/supabase-js';
const supabase = createClient('url', 'anonKey')
const provider = 'provider' as Provider

// ---cut---
await supabase.auth.signInWithOAuth({
  provider,
  options: {
    redirectTo: `http://example.com/auth/callback`,
  },
})
```

</TabPanel>

<TabPanel id="server" label="Server">

In the server, you need to handle the redirect to the OAuth provider's authentication endpoint. The `signInWithOAuth` method returns the endpoint URL, which you can redirect to.

```js
import { createClient, type Provider } from '@supabase/supabase-js'
const supabase = createClient('url', 'anonKey')
const provider = 'provider' as Provider
const redirect = (url: string) => {}

// ---cut---
const { data, error } = await supabase.auth.signInWithOAuth({
  provider,
  options: {
    redirectTo: 'http://example.com/auth/callback',
  },
})

if (data.url) {
  redirect(data.url) // use the redirect API for your server framework
}
```

</TabPanel>

</Tabs>

At the callback endpoint, handle the code exchange to save the user session.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="nextjs"
  queryGroup="framework"
>
<TabPanel id="nextjs" label="Next.js">

Create a new file at `app/auth/callback/route.ts` and populate with the following:

```ts name=app/auth/callback/route.ts
import { NextResponse } from 'next/server'
// The client you created from the Server-Side Auth instructions
import { createClient } from '@/utils/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  // if "next" is in param, use it as the redirect URL
  let next = searchParams.get('next') ?? '/'
  if (!next.startsWith('/')) {
    // if "next" is not a relative URL, use the default
    next = '/'
  }

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
      const isLocalEnv = process.env.NODE_ENV === 'development'
      if (isLocalEnv) {
        // we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
        return NextResponse.redirect(`${origin}${next}`)
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`)
      } else {
        return NextResponse.redirect(`${origin}${next}`)
      }
    }
  }

  // return the user to an error page with instructions
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
```

</TabPanel>
<TabPanel id="sveltekit" label="SvelteKit">

Create a new file at `src/routes/auth/callback/+server.js` and populate with the following:

```js name=src/routes/auth/callback/+server.js
import { redirect } from '@sveltejs/kit';

export const GET = async (event) => {
	const {
		url,
		locals: { supabase }
	} = event;
	const code = url.searchParams.get('code') as string;
	const next = url.searchParams.get('next') ?? '/';

  if (code) {
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      redirect(303, `/${next.slice(1)}`);
    }
  }

  // return the user to an error page with instructions
  redirect(303, '/auth/auth-code-error');
};
```

</TabPanel>

<TabPanel id="astro" label="Astro">

Create a new file at `src/pages/auth/callback.ts` and populate with the following:

```ts name=src/pages/auth/callback.ts
import { createServerClient, parseCookieHeader } from '@supabase/ssr'
import { type APIRoute } from 'astro'

export const GET: APIRoute = async ({ request, cookies, redirect }) => {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  const next = requestUrl.searchParams.get('next') || '/'

  if (code) {
    const supabase = createServerClient(
      import.meta.env.PUBLIC_SUPABASE_URL,
      import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY,
      {
        cookies: {
          getAll() {
            return parseCookieHeader(Astro.request.headers.get('Cookie') ?? '')
          },
          setAll(cookiesToSet) {
            cookiesToSet.forEach(({ name, value, options }) =>
              Astro.cookies.set(name, value, options)
            )
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return redirect(next)
    }
  }

  // return the user to an error page with instructions
  return redirect('/auth/auth-code-error')
}
```

</TabPanel>

<TabPanel id="remix" label="Remix">

Create a new file at `app/routes/auth.callback.tsx` and populate with the following:

```ts name=app/routes/auth.callback.tsx
import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'

export async function loader({ request }: LoaderFunctionArgs) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  const next = requestUrl.searchParams.get('next') || '/'
  const headers = new Headers()

  if (code) {
    const supabase = createServerClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_PUBLISHABLE_KEY!,
      {
        cookies: {
          getAll() {
            return parseCookieHeader(request.headers.get('Cookie') ?? '')
          },
          setAll(cookiesToSet) {
            cookiesToSet.forEach(({ name, value, options }) =>
              headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
            )
          },
        },
      }
    )

    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return redirect(next, { headers })
    }
  }

  // return the user to an error page with instructions
  return redirect('/auth/auth-code-error', { headers })
}
```

</TabPanel>
<TabPanel id="express" label="Express">

Create a new route in your express app and populate with the following:

```js name=app.js
...
app.get("/auth/callback", async function (req, res) {
  const code = req.query.code
  const next = req.query.next ?? "/"

  if (code) {
    const supabase = createServerClient(
      process.env.SUPABASE_URL,
      process.env.SUPABASE_PUBLISHABLE_KEY, {
    cookies: {
      getAll() {
        return parseCookieHeader(context.req.headers.cookie ?? '')
      },
      setAll(cookiesToSet) {
        cookiesToSet.forEach(({ name, value, options }) =>
          context.res.appendHeader('Set-Cookie', serializeCookieHeader(name, value, options))
        )
      },
    },
  })
    await supabase.auth.exchangeCodeForSession(code)
  }

  res.redirect(303, `/${next.slice(1)}`)
})
```

</TabPanel>
</Tabs>


path: references/_partials/postgres_installation.mdx
content: <Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="windows"
>

<TabPanel id="windows" label="Windows">

<StepHikeCompact>

  <StepHikeCompact.Step step={1}>

    <StepHikeCompact.Details title="Install Postgres" fullWidth>

    Download and run the installation file for the latest version from the [Postgres installer download page](https://www.postgresql.org/download/windows/).

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={2}>

    <StepHikeCompact.Details title="Add Postgres to your system PATH" fullWidth>

    Add the Postgres binary to your system PATH.

    In Control Panel, under the Advanced tab of System Properties, click Environment Variables. Edit the Path variable by adding the path to the SQL binary you just installed.

    The path will look something like this, though it may differ slightly depending on your installed version:

    ```
    C:\Program Files\PostgreSQL\17\bin
    ```

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>

    <StepHikeCompact.Details title="Verify that psql is working" fullWidth>

    Open your terminal and run the following command:

    ```sh
    psql --version
    ```

    <Admonition type="tip">

    If you get an error that psql is not available or cannot be found, check that you have correctly added the binary to your system PATH. Also try restarting your terminal.

    </Admonition>

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

</StepHikeCompact>

</TabPanel>

<TabPanel id="mac" label="MacOS">

<StepHikeCompact>

  <StepHikeCompact.Step step={1}>

    <StepHikeCompact.Details title="Install Homebrew" fullWidth>

    Install [Homebrew](https://brew.sh/).

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={2}>

    <StepHikeCompact.Details title="Install Postgres" fullWidth>

    Install Postgres via Homebrew by running the following command in your terminal:

    ```sh
    brew install postgresql@17
    ```

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>

    <StepHikeCompact.Details title="Verify that psql is working" fullWidth>

    Restart your terminal and run the following command:

    ```sh
    psql --version
    ```

    If you get an error that psql is not available or cannot be found then the PATH variable is likely either not correctly set or you need to restart your terminal.

    You can add the Postgres installation path to your PATH variable by running the following command:

    ```sh
    brew info postgresql@17
    ```

    The above command will give an output like this:

    ```sh
    If you need to have postgresql@17 first in your PATH, run:

    echo 'export PATH="/opt/homebrew/opt/postgresql@17/bin:$PATH"' >> ~/.zshrc
    ```

    Run the command mentioned and restart the terminal.

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

</StepHikeCompact>

</TabPanel>

</Tabs>


path: references/_partials/product_management_sql_template.mdx
content: ```sql
-- Create a table for public profiles

create table
  public.products (
    id uuid not null default gen_random_uuid (),
    name text not null,
    price real not null,
    image text null,
    constraint products_pkey primary key (id)
  ) tablespace pg_default;

-- Set up Storage!
insert into storage.buckets (id, name)
  values ('Product Image', 'Product Image');

-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
CREATE POLICY "Enable read access for all users" ON "storage"."objects"
AS PERMISSIVE FOR SELECT
TO public
USING (true)

CREATE POLICY "Enable insert for all users" ON "storage"."objects"
AS PERMISSIVE FOR INSERT
TO authenticated, anon
WITH CHECK (true)

CREATE POLICY "Enable update for all users" ON "storage"."objects"
AS PERMISSIVE FOR UPDATE
TO public
USING (true)
WITH CHECK (true)

```


path: references/_partials/project_setup.mdx
content: ## Project setup

Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.

### Create a project

1. [Create a new project](/dashboard) in the Supabase Dashboard.
2. Enter your project details.
3. Wait for the new database to launch.

### Set up the database schema

Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="dashboard"
  queryGroup="database-method"
>
<TabPanel id="dashboard" label="Dashboard">

1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.
2. Click **User Management Starter** under the **Community > Quickstarts** tab.
3. Click **Run**.

<Admonition type="note">

You can pull the database schema down to your local project by running the `db pull` command. Read the [local development docs](/docs/guides/cli/local-development#link-your-project) for detailed instructions.

```bash
supabase link --project-ref <project-id>
# You can get <project-id> from your project's dashboard URL: https://supabase.com/dashboard/project/<project-id>
supabase db pull
```

</Admonition>

</TabPanel>
<TabPanel id="sql" label="SQL">

<Admonition type="note">

When working locally you can run the following command to create a new migration file:

</Admonition>

```bash
supabase migration new user_management_starter
```

<$Partial path="user_management_quickstart_sql_template.mdx" />

</TabPanel>
</Tabs>

<$Partial path="api_settings.mdx" variables={{ "framework": "{{ .framework }}", "tab": "{{ .tab }}" }}
/>


path: references/_partials/providers.mdx
content: ## Providers

Supabase Auth works with many popular Auth methods, including Social and Phone Auth using third-party providers. See the following sections for a list of supported third-party providers.

### Social Auth

<AuthProviders type="social" />

### Phone Auth

<AuthProviders type="phone" />


path: references/_partials/quickstart_db_setup.mdx
content: <StepHikeCompact.Details title="Create a Supabase project">

Go to [database.new](https://database.new) and create a new Supabase project.

Alternatively, you can create a project using the Management API:

</StepHikeCompact.Details>

<StepHikeCompact.Code>

```bash
# First, get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"

# List your organizations to get the organization ID
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  https://api.supabase.com/v1/organizations

# Create a new project (replace <org-id> with your organization ID)
curl -X POST https://api.supabase.com/v1/projects \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "<org-id>",
    "name": "My Project",
    "region": "us-east-1",
    "db_pass": "<your-secure-password>"
  }'
```

</StepHikeCompact.Code>

<StepHikeCompact.Details>

When your project is up and running, go to the [Table Editor](/dashboard/project/_/editor), create a new table and insert some data.

Alternatively, you can run the following snippet in your project's [SQL Editor](/dashboard/project/_/sql/new). This will create an `instruments` table with some sample data.

</StepHikeCompact.Details>

<StepHikeCompact.Code>

    ```sql SQL_EDITOR
    -- Create the table
    create table instruments (
      id bigint primary key generated always as identity,
      name text not null
    );
    -- Insert some sample data into the table
    insert into instruments (name)
    values
      ('violin'),
      ('viola'),
      ('cello');

    alter table instruments enable row level security;
    ```

</StepHikeCompact.Code>

<StepHikeCompact.Details>

Make the data in your table publicly readable by adding an RLS policy:

</StepHikeCompact.Details>

<StepHikeCompact.Code>

    ```sql SQL_EDITOR
    create policy "public can read instruments"
    on public.instruments
    for select to anon
    using (true);
    ```

</StepHikeCompact.Code>


path: references/_partials/quickstart_intro.mdx
content: This tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:

- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information.
- [Supabase Auth](/docs/guides/auth) - allow users to sign up and log in.
- [Supabase Storage](/docs/guides/storage) - allow users to upload a profile photo.


path: references/_partials/social_provider_settings_supabase.mdx
content: - Go to your [Supabase Project Dashboard](/dashboard)
- In the left sidebar, click the `Authentication` icon (near the top)
- Click on [`Providers`](/dashboard/project/_/auth/providers) under the Configuration section
- Click on **{{ .provider }}** from the accordion list to expand and turn **{{ .provider }} Enabled** to ON
- Enter your **{{ .provider }} Client ID** and **{{ .provider }} Client Secret** saved in the previous step
- Click `Save`


path: references/_partials/social_provider_setup.mdx
content: The next step requires a callback URL, which looks like this: `https://<project-ref>.supabase.co/auth/v1/callback`

- Go to your [Supabase Project Dashboard](/dashboard)
- Click on the `Authentication` icon in the left sidebar
- Click on [`Sign In / Providers`](/dashboard/project/_/auth/providers) under the Configuration section
- Click on **{{ .provider }}** from the accordion list to expand and you'll find your **Callback URL**, you can click `Copy` to copy it to the clipboard

<Admonition type="note">

#### Local development

When testing OAuth locally with the Supabase CLI, ensure your OAuth provider
is configured with the local Supabase Auth callback URL:

http://localhost:54321/auth/v1/callback

If this callback URL is missing or misconfigured, OAuth sign-in may fail or not redirect correctly during local development.

See the [local development docs](/docs/guides/local-development) for more details.

For testing OAuth locally with the Supabase CLI see the [local development docs](/docs/guides/local-development).

</Admonition>


path: references/_partials/uiLibCta.mdx
content: <Admonition type="tip" title="Explore drop-in UI components for your Supabase app.">

UI components built on shadcn/ui that connect to Supabase via a single command.

<Button type="primary" asChild>
  <a href="https://supabase.com/ui">Explore Components</a>
</Button>

</Admonition>


path: references/_partials/universal_links_apple.mdx
content: ### Universal links

For a better user experience, you can use **Universal Links** instead of custom URL schemes. Universal Links allow your app to open directly from web links without showing a browser redirect prompt.

To enable Universal Links, you need to:

1. **Configure Associated Domains** in your Xcode project:

   - Add your domain to the Associated Domains capability
   - Format: `applinks:yourdomain.com`

2. **Host the Apple App Site Association (AASA) file** on your own infrastructure:
   - The file must be accessible at `https://yourdomain.com/.well-known/apple-app-site-association` or `https://yourdomain.com/apple-app-site-association`
   - The file must be served with `Content-Type: application/json` (or `text/json`)
   - The file must be accessible over HTTPS without redirects
   - The file should not have a file extension

<Admonition type="caution">

Supabase does not currently support hosting the AASA file. You must host this file on your own
infrastructure following [Apple's best
practices](https://developer.apple.com/documentation/xcode/supporting-associated-domains).

</Admonition>

The AASA file format should look like this:

```json
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAM_ID.BUNDLE_ID",
        "paths": ["*"]
      }
    ]
  }
}
```

Replace `TEAM_ID` with your Apple Developer Team ID and `BUNDLE_ID` with your app's bundle identifier.

For detailed setup instructions, see [Apple's documentation on supporting associated domains](https://developer.apple.com/documentation/xcode/supporting-associated-domains).


path: references/_partials/user_management_quickstart_sql_template.mdx
content: ```sql
-- Create a table for public profiles
create table profiles (
  id uuid references auth.users not null primary key,
  updated_at timestamp with time zone,
  username text unique,
  full_name text,
  avatar_url text,
  website text,

  constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/database/postgres/row-level-security for more details.
alter table profiles
  enable row level security;

create policy "Public profiles are viewable by everyone." on profiles
  for select using (true);

create policy "Users can insert their own profile." on profiles
  for insert with check ((select auth.uid()) = id);

create policy "Users can update own profile." on profiles
  for update using ((select auth.uid()) = id);

-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger
set search_path = ''
as $$
begin
  insert into public.profiles (id, full_name, avatar_url)
  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
  return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

-- Set up Storage!
insert into storage.buckets (id, name)
  values ('avatars', 'avatars');

-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage/security/access-control#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
  for select using (bucket_id = 'avatars');

create policy "Anyone can upload an avatar." on storage.objects
  for insert with check (bucket_id = 'avatars');

create policy "Anyone can update their own avatar." on storage.objects
  for update using ((select auth.uid()) = owner) with check (bucket_id = 'avatars');
```


path: references/errorCodes/authErrorCodes.toml
content: # Official error codes for Supabase Auth
#
# Error codes should be documented in the following format
#
# [error_code]
# description = "Error description."
# resolution = "How to resolve this error."
# [[error_code.references]]
# href = "https://supabase.com/docs/some/relevant/guide"
# description = "Guide for doing some relevant thing"
#
# error_code should be a unique and stable identifier for the error, that the
# developer can match against for error handling.

[anonymous_provider_disabled]
description = "Anonymous sign-ins are disabled."

[bad_code_verifier]
description = "Returned from the PKCE flow where the provided code verifier does not match the expected one. Indicates a bug in the implementation of the client library."

[bad_json]
description = "Usually used when the HTTP body of the request is not valid JSON."

[bad_jwt]
description = "JWT sent in the Authorization header is not valid."

[bad_oauth_callback]
description = "OAuth callback from provider to Auth does not have all the required attributes (state). Indicates an issue with the OAuth provider or client library implementation."

[bad_oauth_state]
description = "OAuth state (data echoed back by the OAuth provider to Supabase Auth) is not in the correct format. Indicates an issue with the OAuth provider integration."

[captcha_failed]
description = "CAPTCHA challenge could not be verified with the CAPTCHA provider. Check your CAPTCHA integration."

[conflict]
description = "General database conflict, such as concurrent requests on resources that should not be modified concurrently. Can often occur when you have too many session refresh requests firing off at the same time for a user. Check your app for concurrency issues, and if detected, back off exponentially."

[email_address_invalid]
description = "Example and test domains are currently not supported. Use a different email address."

[email_address_not_authorized]
description = "Email sending is not allowed for this address as your project is using the default SMTP service. Emails can only be sent to members in your Supabase organization. If you want to send emails to others, set up a custom SMTP provider."
[[email_address_not_authorized.references]]
href = "https://supabase.com/docs/guides/auth/auth-smtp"
description = "Setting up a custom SMTP provider"

[email_conflict_identity_not_deletable]
description = "Unlinking this identity causes the user's account to change to an email address which is already used by another user account. Indicates an issue where the user has two different accounts using different primary email addresses. You may need to migrate user data to one of their accounts in this case."

[email_exists]
description = "Email address already exists in the system."

[email_not_confirmed]
description = "Signing in is not allowed for this user as the email address is not confirmed."

[email_provider_disabled]
description = "Signups are disabled for email and password."

[flow_state_expired]
description = "PKCE flow state to which the API request relates has expired. Ask the user to sign in again."

[flow_state_not_found]
description = "PKCE flow state to which the API request relates no longer exists. Flow states expire after a while and are progressively cleaned up, which can cause this error. Retried requests can cause this error, as the previous request likely destroyed the flow state. Ask the user to sign in again."

[hook_payload_invalid_content_type]
description = "Payload from Auth does not have a valid Content-Type header."

[hook_payload_over_size_limit]
description = "Payload from Auth exceeds maximum size limit."

[hook_timeout]
description = "Unable to reach hook within maximum time allocated."

[hook_timeout_after_retry]
description = "Unable to reach hook after maximum number of retries."

[identity_already_exists]
description = "The identity to which the API relates is already linked to a user."

[identity_not_found]
description = "Identity to which the API call relates does not exist, such as when an identity is unlinked or deleted."

[insufficient_aal]
description = "To call this API, the user must have a higher Authenticator Assurance Level. To resolve, ask the user to solve an MFA challenge."
[[insufficient_aal.references]]
href = "https://supabase.com/docs/guides/auth/auth-mfa"
description = "MFA"

[invite_not_found]
description = "Invite is expired or already used."

[invalid_credentials]
description = "Login credentials or grant type not recognized."

[manual_linking_disabled]
description = "Calling the supabase.auth.linkUser() and related APIs is not enabled on the Auth server."

[mfa_challenge_expired]
description = "Responding to an MFA challenge should happen within a fixed time period. Request a new challenge when encountering this error."

[mfa_factor_name_conflict]
description = "MFA factors for a single user should not have the same friendly name."

[mfa_factor_not_found]
description = "MFA factor no longer exists."

[mfa_ip_address_mismatch]
description = "The enrollment process for MFA factors must begin and end with the same IP address."

[mfa_phone_enroll_not_enabled]
description = "Enrollment of MFA Phone factors is disabled."

[mfa_phone_verify_not_enabled]
description = "Login via Phone factors and verification of new Phone factors is disabled."

[mfa_totp_enroll_not_enabled]
description = "Enrollment of MFA TOTP factors is disabled."

[mfa_totp_verify_not_enabled]
description = "Login via TOTP factors and verification of new TOTP factors is disabled."

[mfa_verification_failed]
description = "MFA challenge could not be verified -- wrong TOTP code."

[mfa_verification_rejected]
description = "Further MFA verification is rejected. Only returned if the MFA verification attempt hook returns a reject decision."
[[mfa_verification_rejected.references]]
href = "https://supabase.com/docs/guides/auth/auth-hooks?language=add-admin-role#hook-mfa-verification-attempt"
description = "MFA verification hook"

[mfa_verified_factor_exists]
description = "Verified phone factor already exists for a user. Unenroll existing verified phone factor to continue."

[mfa_web_authn_enroll_not_enabled]
description = "Enrollment of MFA Web Authn factors is disabled."

[mfa_web_authn_verify_not_enabled]
description = "Login via WebAuthn factors and verification of new WebAuthn factors is disabled."

[no_authorization]
description = "This HTTP request requires an Authorization header, which is not provided."

[not_admin]
description = "User accessing the API is not admin, i.e. the JWT does not contain a role claim that identifies them as an admin of the Auth server."

[oauth_provider_not_supported]
description = "Using an OAuth provider which is disabled on the Auth server."

[otp_disabled]
description = "Sign in with OTPs (magic link, email OTP) is disabled. Check your server's configuration."

[otp_expired]
description = "OTP code for this sign-in has expired. Ask the user to sign in again."

[over_email_send_rate_limit]
description = "Too many emails have been sent to this email address. Ask the user to wait a while before trying again."

[over_request_rate_limit]
description = "Too many requests have been sent by this client (IP address). Ask the user to try again in a few minutes. Sometimes can indicate a bug in your application that mistakenly sends out too many requests (such as a badly written useEffect React hook)."
[[over_request_rate_limit.references]]
href = "https://react.dev/reference/react/useEffect"
description = "React useEffect hook"

[over_sms_send_rate_limit]
description = "Too many SMS messages have been sent to this phone number. Ask the user to wait a while before trying again."

[phone_exists]
description = "Phone number already exists in the system."

[phone_not_confirmed]
description = "Signing in is not allowed for this user as the phone number is not confirmed."

[phone_provider_disabled]
description = "Signups are disabled for phone and password."

[provider_disabled]
description = "OAuth provider is disabled for use. Check your server's configuration."

[provider_email_needs_verification]
description = "Not all OAuth providers verify their user's email address. Supabase Auth requires emails to be verified, so this error is sent out when a verification email is sent after completing the OAuth flow."

[reauthentication_needed]
description = "A user needs to reauthenticate to change their password. Ask the user to reauthenticate by calling the supabase.auth.reauthenticate() API."

[reauthentication_not_valid]
description = "Verifying a reauthentication failed, the code is incorrect. Ask the user to enter a new code."

[refresh_token_not_found]
description = "Session containing the refresh token not found."

[refresh_token_already_used]
description = "Refresh token has been revoked and falls outside the refresh token reuse interval. See the documentation on sessions for further information."
[[refresh_token_already_used.references]]
href = "https://supabase.com/docs/guides/auth/sessions"
description = "Auth sessions"

[request_timeout]
description = "Processing the request took too long. Retry the request."

[same_password]
description = "A user that is updating their password must use a different password than the one currently used."

[saml_assertion_no_email]
description = "SAML assertion (user information) was received after sign in, but no email address was found in it, which is required. Check the provider's attribute mapping and/or configuration."

[saml_assertion_no_user_id]
description = "SAML assertion (user information) was received after sign in, but a user ID (called NameID) was not found in it, which is required. Check the SAML identity provider's configuration."

[saml_entity_id_mismatch]
description = "(Admin API.) Updating the SAML metadata for a SAML identity provider is not possible, as the entity ID in the update does not match the entity ID in the database. This is equivalent to creating a new identity provider, and you should do that instead."

[saml_idp_already_exists]
description = "(Admin API.) Adding a SAML identity provider that is already added."

[saml_idp_not_found]
description = "SAML identity provider not found. Most often returned after IdP-initiated sign-in with an unregistered SAML identity provider in Supabase Auth."

[saml_metadata_fetch_failed]
description = "(Admin API.) Adding or updating a SAML provider failed as its metadata could not be fetched from the provided URL."

[saml_provider_disabled]
description = "Using Enterprise SSO with SAML 2.0 is not enabled on the Auth server."
[[saml_provider_disabled.references]]
href = "https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml"
description = "Enterprise SSO"

[saml_relay_state_expired]
description = "SAML relay state is an object that tracks the progress of a supabase.auth.signInWithSSO() request. The SAML identity provider should respond after a fixed amount of time, after which this error is shown. Ask the user to sign in again."

[saml_relay_state_not_found]
description = "SAML relay states are progressively cleaned up after they expire, which can cause this error. Ask the user to sign in again."

[session_expired]
description = "Session to which the API request relates has expired. This can occur if an inactivity timeout is configured, or the session entry has exceeded the configured timebox value. See the documentation on sessions for more information."
[[session_expired.references]]
href = "https://supabase.com/docs/guides/auth/sessions"
description = "Auth sessions"

[session_not_found]
description = "Session to which the API request relates no longer exists. This can occur if the user has signed out, or the session entry in the database was deleted in some other way."

[signup_disabled]
description = "Sign ups (new account creation) are disabled on the server."

[single_identity_not_deletable]
description = "Every user must have at least one identity attached to it, so deleting (unlinking) an identity is not allowed if it's the only one for the user."

[sms_send_failed]
description = "Sending an SMS message failed. Check your SMS provider configuration."

[sso_domain_already_exists]
description = "(Admin API.) Only one SSO domain can be registered per SSO identity provider."

[sso_provider_not_found]
description = "SSO provider not found. Check the arguments in supabase.auth.signInWithSSO()."

[too_many_enrolled_mfa_factors]
description = "A user can only have a fixed number of enrolled MFA factors."

[unexpected_audience]
description = "(Deprecated feature not available via Supabase client libraries.) The request's X-JWT-AUD claim does not match the JWT's audience."

[unexpected_failure]
description = "Auth service is degraded or a bug is present, without a specific reason."

[user_already_exists]
description = "User with this information (email address, phone number) cannot be created again as it already exists."

[user_banned]
description = "User to which the API request relates has a banned_until property which is still active. No further API requests should be attempted until this field is cleared."

[user_not_found]
description = "User to which the API request relates no longer exists."

[user_sso_managed]
description = "When a user comes from SSO, certain fields of the user cannot be updated (like email)."

[validation_failed]
description = "Provided parameters are not in the expected format."

[weak_password]
description = "User is signing up or changing their password without meeting the password strength criteria. Use the AuthWeakPasswordError class to access more information about what they need to do to make the password pass."


path: references/errorCodes/realtimeErrorCodes.toml
content: # Official error codes for Supabase Realtime
#
# Error codes should be documented in the following format
#
# [error_code]
# description = "Error description."
# resolution = "How to resolve this error."
# [[error_code.references]]
# href = "https://supabase.com/docs/some/relevant/guide"
# description = "Guide for doing some relevant thing"
#
# error_code should be a unique and stable identifier for the error, that the
# developer can match against for error handling.

[TopicNameRequired]
description = "You are trying to use Realtime without a topic name set."

[RealtimeDisabledForConfiguration]
description = "The configuration provided to Realtime on connect will not be able to provide you any Postgres Changes."
resolution = "Verify your configuration on channel startup as you might not have your tables properly registered."

[TenantNotFound]
description = "The tenant you are trying to connect to does not exist."
resolution = "Verify the tenant name you are trying to connect to exists in the realtime.tenants table."

[ErrorConnectingToWebsocket]
description = "Error when trying to connect to the WebSocket server."
resolution = "Verify user information on connect."

[ErrorAuthorizingWebsocket]
description = "Error when trying to authorize the WebSocket connection."
resolution = "Verify user information on connect."

[TableHasSpacesInName]
description = "The table you are trying to listen to has spaces in its name which we are unable to support."
resolution = "Change the table name to not have spaces in it."

[UnableToDeleteTenant]
description = "Error when trying to delete a tenant."

[UnableToSetPolicies]
description = "Error when setting up Authorization Policies."

[UnableCheckoutConnection]
description = "Error when trying to checkout a connection from the tenant pool."

[UnableToSubscribeToPostgres]
description = "Error when trying to subscribe to Postgres changes."

[ReconnectSubscribeToPostgres]
description = "Postgres changes still waiting to be subscribed."

[ChannelRateLimitReached]
description = "The number of channels you can create has reached its limit."

[ConnectionRateLimitReached]
description = "The number of connected clients has reached its limit."

[ClientJoinRateLimitReached]
description = "The rate of joins per second from your clients has reached the channel limits."

[RealtimeDisabledForTenant]
description = "Realtime has been disabled for the tenant."

[UnableToConnectToTenantDatabase]
description = "Realtime was not able to connect to the tenant's database."

[DatabaseLackOfConnections]
description = "Realtime was not able to connect to the tenant's database due to not having enough available connections."
resolution = "Verify your database connection limits."
[[DatabaseLackOfConnections.references]]
href = "https://supabase.com/docs/guides/database/connection-management"
description = "Connection management guide"

[RealtimeNodeDisconnected]
description = "Realtime is a distributed application and this means that one the system is unable to communicate with one of the distributed nodes."

[MigrationsFailedToRun]
description = "Error when running the migrations against the Tenant database that are required by Realtime."

[StartListenAndReplicationFailed]
description = "Error when starting the replication and listening of errors for database broadcasting."

[ReplicationMaxWalSendersReached]
description = "Maximum number of WAL senders reached in tenant database."
[[ReplicationMaxWalSendersReached.references]]
href = "https://supabase.com/docs/guides/database/custom-postgres-config#cli-configurable-settings"
description = "Configuring max WAL senders"

[MigrationCheckFailed]
description = "Check to see if we require to run migrations fails."

[PartitionCreationFailed]
description = "Error when creating partitions for realtime.messages."

[ErrorStartingPostgresCDCStream]
description = "Error when starting the Postgres CDC stream which is used for Postgres Changes."

[UnknownDataProcessed]
description = "An unknown data type was processed by the Realtime system."

[ErrorStartingPostgresCDC]
description = "Error when starting the Postgres CDC extension which is used for Postgres Changes."

[ReplicationSlotBeingUsed]
description = "The replication slot is being used by another transaction."

[PoolingReplicationPreparationError]
description = "Error when preparing the replication slot."

[PoolingReplicationError]
description = "Error when pooling the replication slot."

[SubscriptionDeletionFailed]
description = "Error when trying to delete a subscription for postgres changes."

[UnableToDeletePhantomSubscriptions]
description = "Error when trying to delete subscriptions that are no longer being used."

[UnableToCheckProcessesOnRemoteNode]
description = "Error when trying to check the processes on a remote node."

[UnableToCreateCounter]
description = "Error when trying to create a counter to track rate limits for a tenant."

[UnableToIncrementCounter]
description = "Error when trying to increment a counter to track rate limits for a tenant."

[UnableToDecrementCounter]
description = "Error when trying to decrement a counter to track rate limits for a tenant."

[UnableToUpdateCounter]
description = "Error when trying to update a counter to track rate limits for a tenant."

[UnableToFindCounter]
description = "Error when trying to find a counter to track rate limits for a tenant."

[UnhandledProcessMessage]
description = "Unhandled message received by a Realtime process."

[UnableToTrackPresence]
description = "Error when handling track presence for this socket."

[UnknownPresenceEvent]
description = "Presence event type not recognized by service."

[IncreaseConnectionPool]
description = "The number of connections you have set for Realtime are not enough to handle your current use case."

[RlsPolicyError]
description = "Error on RLS policy used for authorization."

[ConnectionInitializing]
description = "Database is initializing connection."

[DatabaseConnectionIssue]
description = "Database had connection issues and connection was not able to be established."

[UnableToConnectToProject]
description = "Unable to connect to Project database."

[InvalidJWTExpiration]
description = "JWT exp claim value it's incorrect."

[JwtSignatureError]
description = "JWT signature was not able to be validated."

[MalformedJWT]
description = "Token received does not comply with the JWT format."

[Unauthorized]
description = "Unauthorized access to Realtime channel."

[RealtimeRestarting]
description = "Realtime is currently restarting."

[UnableToProcessListenPayload]
description = "Payload sent in NOTIFY operation was JSON parsable."

[UnableToListenToTenantDatabase]
description = "Unable to LISTEN for notifications against the Tenant Database."

[UnprocessableEntity]
description = "Received a HTTP request with a body that was not able to be processed by the endpoint."

[InitializingProjectConnection]
description = "Connection against Tenant database is still starting."

[TimeoutOnRpcCall]
description = "RPC request within the Realtime server has timed out."

[ErrorOnRpcCall]
description = "Error when calling another realtime node."

[ErrorExecutingTransaction]
description = "Error executing a database transaction in tenant database."

[SynInitializationError]
description = "Our framework to syncronize processes has failed to properly startup a connection to the database."

[JanitorFailedToDeleteOldMessages]
description = "Scheduled task for realtime.message cleanup was unable to run."

[UnableToEncodeJson]
description = "An error were we are not handling correctly the response to be sent to the end user."

[UnknownErrorOnController]
description = "An error we are not handling correctly was triggered on a controller."

[UnknownErrorOnChannel]
description = "An error we are not handling correctly was triggered on a channel."


path: references/guides/ai.mdx
content: ---
id: 'ai'
title: 'AI & Vectors'
description: 'The best vector database is the database you already have.'
subtitle: 'The best vector database is the database you already have.'
hideToc: true
---

Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale.

The toolkit includes:

- A [vector store](/docs/guides/ai/vector-columns) and embeddings support using Postgres and pgvector.
- A [Python client](/docs/guides/ai/vecs-python-client) for managing unstructured embeddings.
- An [embedding generation](/docs/guides/ai/quickstarts/generate-text-embeddings) process using open source models directly in Edge Functions.
- [Database migrations](/docs/guides/ai/examples/headless-vector-search#prepare-your-database) for managing structured embeddings.
- Integrations with all popular AI providers, such as [OpenAI](/docs/guides/ai/examples/openai), [Hugging Face](/docs/guides/ai/hugging-face), [LangChain](/docs/guides/ai/langchain), and more.

## Search

You can use Supabase to build different types of search features for your app, including:

- [Semantic search](/docs/guides/ai/semantic-search): search by meaning rather than exact keywords
- [Keyword search](/docs/guides/ai/keyword-search): search by words or phrases
- [Hybrid search](/docs/guides/ai/hybrid-search): combine semantic search with keyword search

## Examples

Check out all of the AI [templates and examples](https://github.com/supabase/supabase/tree/master/examples/ai) in our GitHub repository.

{/* <!-- vale off --> */}

<div className="grid md:grid-cols-12 gap-4 not-prose">
  {examples.map((x) => (
    <div className="col-span-4" key={x.href}>
      <Link href={x.href} passHref>
        <GlassPanel icon={'/docs/img/icons/github-icon'} hasLightIcon={true} title={x.name}>
          {x.description}
        </GlassPanel>
      </Link>
    </div>
  ))}
</div>

export const examples = [
  {
    name: 'Headless Vector Search',
    description: 'A toolkit to perform vector similarity search on your knowledge base embeddings.',
    href: '/guides/ai/examples/headless-vector-search',
  },
  {
    name: 'Image Search with OpenAI CLIP',
    description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.',
    href: '/guides/ai/examples/image-search-openai-clip',
  },
  {
    name: 'Hugging Face inference',
    description: 'Generate image captions using Hugging Face.',
    href: '/guides/ai/examples/huggingface-image-captioning',
  },
  {
    name: 'OpenAI completions',
    description: 'Generate GPT text completions using OpenAI in Edge Functions.',
    href: '/guides/ai/examples/openai',
  },
  {
    name: 'Building ChatGPT Plugins',
    description: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.',
    href: '/guides/ai/examples/building-chatgpt-plugins',
  },
  {
    name: 'Vector search with Next.js and OpenAI',
    description:
      'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.',
    href: '/guides/ai/examples/nextjs-vector-search',
  },
]

{/* <!-- vale on --> */}

## Integrations

{/* <!-- vale off --> */}

<div className="grid md:grid-cols-12 gap-4 not-prose">
  {integrations.map((x) => (
    <div className="col-span-4" key={x.href}>
      <Link href={x.href} passHref>
        <GlassPanel title={x.name}>{x.description}</GlassPanel>
      </Link>
    </div>
  ))}
</div>

export const integrations = [
  {
    name: 'OpenAI',
    description:
      'OpenAI is an AI research and deployment company. Supabase provides a simple way to use OpenAI in your applications.',
    href: '/guides/ai/examples/building-chatgpt-plugins',
  },
  {
    name: 'Amazon Bedrock',
    description:
      'A fully managed service that offers a choice of high-performing foundation models from leading AI companies.',
    href: '/guides/ai/integrations/amazon-bedrock',
  },
  {
    name: 'Hugging Face',
    description:
      "Hugging Face is an open-source provider of NLP technologies. Supabase provides a simple way to use Hugging Face's models in your applications.",
    href: '/guides/ai/hugging-face',
  },
  {
    name: 'LangChain',
    description:
      'LangChain is a language-agnostic, open-source, and self-hosted API for text translation, summarization, and sentiment analysis.',
    href: '/guides/ai/langchain',
  },
  {
    name: 'LlamaIndex',
    description: 'LlamaIndex is a data framework for your LLM applications.',
    href: '/guides/ai/integrations/llamaindex',
  },
]

{/* <!-- vale on --> */}

## Case studies

{/* <!-- vale off --> */}

<div className="grid md:grid-cols-12 gap-4 not-prose">
  {[
    {
      name: 'Berri AI Boosts Productivity by Migrating from AWS RDS to Supabase with pgvector',
      description:
        'Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS and successfully migrated to Supabase.',
      href: 'https://supabase.com/customers/berriai',
    },
    {
      name: 'Firecrawl switches from Pinecone to Supabase for PostgreSQL vector embeddings',
      description:
        'How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using Supabase with pgvector',
      href: 'https://supabase.com/customers/firecrawl',
    },
    {
      name: 'Markprompt: GDPR-Compliant AI Chatbots for Docs and Websites',
      description:
        "AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and GDPR-compliant prompt experiences on top of their content, by leveraging Supabase's secure and privacy-focused database and authentication solutions",
      href: 'https://supabase.com/customers/markprompt',
    },
  ].map((x) => (
    <div className="col-span-4" key={x.href}>
      <Link href={x.href} passHref>
        <GlassPanel title={x.name}>{x.description}</GlassPanel>
      </Link>
    </div>
  ))}
</div>

{/* <!-- vale on --> */}


path: references/guides/ai/automatic-embeddings.mdx
content: ---
id: 'ai-automatic-embeddings'
title: 'Automatic embeddings'
description: 'Automate embedding generation and updates in Postgres'
sidebar_label: 'Automatic embeddings'
---

Vector embeddings enable powerful [semantic search](/docs/guides/ai/semantic-search) capabilities in Postgres, but managing them alongside your content has traditionally been complex. This guide demonstrates how to automate embedding generation and updates using Supabase [Edge Functions](/docs/guides/functions), [pgmq](/docs/guides/database/extensions/pgmq), [pg_net](/docs/guides/database/extensions/pg_net), and [pg_cron](/docs/guides/cron).

## Understanding the challenge

When implementing semantic search with pgvector, developers typically need to:

1. Generate embeddings via an external API (like OpenAI)
2. Store these embeddings alongside the content
3. Keep embeddings in sync when content changes
4. Handle failures and retries in the embedding generation process

While Postgres [full-text search](/docs/guides/database/full-text-search) can handle this internally through synchronous calls to `to_tsvector` and [triggers](https://www.postgresql.org/docs/current/textsearch-features.html#TEXTSEARCH-UPDATE-TRIGGERS), semantic search requires asynchronous API calls to a provider like OpenAI to generate vector embeddings. This guide demonstrates how to use triggers, queues, and Supabase Edge Functions to bridge this gap.

## Understanding the architecture

We'll leverage the following Postgres and Supabase features to create the automated embedding system:

1. [pgvector](/docs/guides/database/extensions/pgvector): Stores and queries vector embeddings
1. [pgmq](/docs/guides/queues): Queues embedding generation requests for processing and retries
1. [pg_net](/docs/guides/database/extensions/pg_net): Handles asynchronous HTTP requests to Edge Functions directly from Postgres
1. [pg_cron](/docs/guides/cron): Automatically processes and retries embedding generations
1. [Triggers](/docs/guides/database/postgres/triggers): Detects content changes and enqueues embedding generation requests
1. [Edge Functions](/docs/guides/functions): Generates embeddings via an API like OpenAI (customizable)

We'll design the system to:

1. Be generic, so that it can be used with any table and content. This allows you to configure embeddings in multiple places, each with the ability to customize the input used for embedding generation. These will all use the same queue infrastructure and Edge Function to generate the embeddings.

1. Handle failures gracefully, by retrying failed jobs and providing detailed information about the status of each job.

## Implementation

We'll start by setting up the infrastructure needed to queue and process embedding generation requests. Then we'll create an example table with triggers to enqueue these embedding requests whenever content is inserted or updated.

### Step 1: Enable extensions

First, let's enable the required extensions:

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="sql"
  queryGroup="database-method"
>
<TabPanel id="sql" label="SQL">

```sql
-- For vector operations
create extension if not exists vector
with
  schema extensions;

-- For queueing and processing jobs
-- (pgmq will create its own schema)
create extension if not exists pgmq;

-- For async HTTP requests
create extension if not exists pg_net
with
  schema extensions;

-- For scheduled processing and retries
-- (pg_cron will create its own schema)
create extension if not exists pg_cron;

-- For clearing embeddings during updates
create extension if not exists hstore
with
  schema extensions;
```

Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
To disable an extension, call `drop extension`.

</TabPanel>
<TabPanel id="dashboard" label="Dashboard">

1. Go to the [Extensions](/dashboard/project/_/database/extensions) page in the Dashboard.
2. Search for and enable the following extensions:
   - `vector`
   - `pgmq`
   - `pg_net`
   - `pg_cron`
   - `hstore`

</TabPanel>
</Tabs>

### Step 2: Create utility functions

Before we set up our embedding logic, we need to create some utility functions:

```sql
-- Schema for utility functions
create schema util;

-- Utility function to get the Supabase project URL (required for Edge Functions)
create function util.project_url()
returns text
language plpgsql
security definer
as $$
declare
  secret_value text;
begin
  -- Retrieve the project URL from Vault
  select decrypted_secret into secret_value from vault.decrypted_secrets where name = 'project_url';
  return secret_value;
end;
$$;

-- Generic function to invoke any Edge Function
create or replace function util.invoke_edge_function(
  name text,
  body jsonb,
  timeout_milliseconds int = 5 * 60 * 1000  -- default 5 minute timeout
)
returns void
language plpgsql
as $$
declare
  headers_raw text;
  auth_header text;
begin
  -- If we're in a PostgREST session, reuse the request headers for authorization
  headers_raw := current_setting('request.headers', true);

  -- Only try to parse if headers are present
  auth_header := case
    when headers_raw is not null then
      (headers_raw::json->>'authorization')
    else
      null
  end;

  -- Perform async HTTP request to the edge function
  perform net.http_post(
    url => util.project_url() || '/functions/v1/' || name,
    headers => jsonb_build_object(
      'Content-Type', 'application/json',
      'Authorization', auth_header
    ),
    body => body,
    timeout_milliseconds => timeout_milliseconds
  );
end;
$$;

-- Generic trigger function to clear a column on update
create or replace function util.clear_column()
returns trigger
language plpgsql as $$
declare
    clear_column text := TG_ARGV[0];
begin
    NEW := NEW #= hstore(clear_column, NULL);
    return NEW;
end;
$$;
```

Here we create:

- A schema `util` to store utility functions.
- A function to retrieve the Supabase project URL from [Vault](/docs/guides/database/vault). We'll add this secret next.
- A generic function to invoke any Edge Function with a given name and request body.
- A generic trigger function to clear a column on update. This function accepts the column name as an argument and sets it to `NULL` in the `NEW` record. We'll explain how to use this function later.

Every project has a unique API URL that is required to invoke Edge Functions. Let's go ahead and add the project URL secret to Vault depending on your environment.

When working with a local Supabase stack, add the following to your `supabase/seed.sql` file:

```sql
select
  vault.create_secret('http://api.supabase.internal:8000', 'project_url');
```

When deploying to the cloud platform, open the [SQL editor](/dashboard/project/_/sql/new) and run the following, replacing `<project-url>` with your [project's API URL](/dashboard/project/_/settings/api):

```sql
select
  vault.create_secret('<project-url>', 'project_url');
```

### Step 3: Create queue and triggers

Our goal is to automatically generate embeddings whenever content is inserted or updated within a table. We can use triggers and queues to achieve this. Our approach is to automatically queue embedding jobs whenever records are inserted or updated in a table, then process them asynchronously using a cron job. If a job fails, it will remain in the queue and be retried in the next scheduled task.

First we create a `pgmq` queue for processing embedding requests:

```sql
-- Queue for processing embedding jobs
select pgmq.create('embedding_jobs');
```

Next we create a trigger function to queue embedding jobs. We'll use this function to handle both insert and update events:

```sql
-- Generic trigger function to queue embedding jobs
create or replace function util.queue_embeddings()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
declare
  content_function text = TG_ARGV[0];
  embedding_column text = TG_ARGV[1];
begin
  perform pgmq.send(
    queue_name => 'embedding_jobs',
    msg => jsonb_build_object(
      'id', NEW.id,
      'schema', TG_TABLE_SCHEMA,
      'table', TG_TABLE_NAME,
      'contentFunction', content_function,
      'embeddingColumn', embedding_column
    )
  );
  return NEW;
end;
$$;
```

Our `util.queue_embeddings` trigger function is generic and can be used with any table and content function. It accepts two arguments:

1. `content_function`: The name of a function that returns the text content to be embedded. The function should accept a single row as input and return text (see the `embedding_input` example).

   This allows you to customize the text input passed to the embedding model - for example, you could concatenate multiple columns together like `title` and `content` and use the result as input.

2. `embedding_column`: The name of the destination column where the embedding will be stored.

Note that the `util.queue_embeddings` trigger function requires a `for each row` clause to work correctly. See [Usage](#usage) for an example of how to use this trigger function with your table.

Next we'll create a function to process the embedding jobs. This function will read jobs from the queue, group them into batches, and invoke the Edge Function to generate embeddings. We'll use `pg_cron` to schedule this function to run every 10 seconds.

```sql
-- Function to process embedding jobs from the queue
create or replace function util.process_embeddings(
  batch_size int = 10,
  max_requests int = 10,
  timeout_milliseconds int = 5 * 60 * 1000 -- default 5 minute timeout
)
returns void
language plpgsql
as $$
declare
  job_batches jsonb[];
  batch jsonb;
begin
  with
    -- First get jobs and assign batch numbers
    numbered_jobs as (
      select
        message || jsonb_build_object('jobId', msg_id) as job_info,
        (row_number() over (order by 1) - 1) / batch_size as batch_num
      from pgmq.read(
        queue_name => 'embedding_jobs',
        vt => timeout_milliseconds / 1000,
        qty => max_requests * batch_size
      )
    ),
    -- Then group jobs into batches
    batched_jobs as (
      select
        jsonb_agg(job_info) as batch_array,
        batch_num
      from numbered_jobs
      group by batch_num
    )
  -- Finally aggregate all batches into array
  select array_agg(batch_array)
  from batched_jobs
  into job_batches;

  -- Invoke the embed edge function for each batch
  foreach batch in array job_batches loop
    perform util.invoke_edge_function(
      name => 'embed',
      body => batch,
      timeout_milliseconds => timeout_milliseconds
    );
  end loop;
end;
$$;

-- Schedule the embedding processing
select
  cron.schedule(
    'process-embeddings',
    '10 seconds',
    $$
    select util.process_embeddings();
    $$
  );
```

Let's discuss some common questions about this approach:

#### Why not generate all embeddings in a single Edge Function request?

While this is possible, it can lead to long processing times and potential timeouts. Batching allows us to process multiple embeddings concurrently and handle failures more effectively.

#### Why not one request per row?

This approach can lead to API rate limiting and performance issues. Batching provides a balance between efficiency and reliability.

#### Why queue requests instead of processing them immediately?

Queuing allows us to handle failures gracefully, retry requests, and manage concurrency more effectively. Specifically we are using `pgmq`'s visibility timeouts to ensure that failed requests are retried.

#### How do visibility timeouts work?

Every time we read a message from the queue, we set a visibility timeout which tells `pgmq` to hide the message from other readers for a certain period. If the Edge Function fails to process the message within this period, the message becomes visible again and will be retried by the next scheduled task.

#### How do we handle retries?

We use `pg_cron` to schedule a task that reads messages from the queue and processes them. If the Edge Function fails to process a message, it becomes visible again after a timeout and can be retried by the next scheduled task.

#### Is 10 seconds a good interval for processing?

This interval is a good starting point, but you may need to adjust it based on your workload and the time it takes to generate embeddings. You can adjust the `batch_size`, `max_requests`, and `timeout_milliseconds` parameters to optimize performance.

### Step 4: Create the Edge Function

Finally we'll create the Edge Function to generate embeddings. We'll use OpenAI's API in this example, but you can replace it with any other embedding generation service.

Use the Supabase CLI to create a new Edge Function:

```bash
supabase functions new embed
```

This will create a new directory `supabase/functions/embed` with an `index.ts` file. Replace the contents of this file with the following:

_supabase/functions/embed/index.ts_:

```typescript
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'

// We'll use the OpenAI API to generate embeddings
import OpenAI from 'jsr:@openai/openai'

import { z } from 'npm:zod'

// We'll make a direct Postgres connection to update the document
import postgres from 'https://deno.land/x/postgresjs@v3.4.5/mod.js'

// Initialize OpenAI client
const openai = new OpenAI({
  // We'll need to manually set the `OPENAI_API_KEY` environment variable
  [REDACTED_SECRET]'OPENAI_API_KEY'),
})

// Initialize Postgres client
const sql = postgres(
  // `SUPABASE_DB_URL` is a built-in environment variable
  Deno.env.get('SUPABASE_DB_URL')!
)

const jobSchema = z.object({
  jobId: z.number(),
  id: z.number(),
  schema: z.string(),
  table: z.string(),
  contentFunction: z.string(),
  embeddingColumn: z.string(),
})

const failedJobSchema = jobSchema.extend({
  error: z.string(),
})

type Job = z.infer<typeof jobSchema>
type FailedJob = z.infer<typeof failedJobSchema>

type Row = {
  id: string
  content: unknown
}

const QUEUE_NAME = 'embedding_jobs'

// Listen for HTTP requests
Deno.serve(async (req) => {
  if (req.method !== 'POST') {
    return new Response('expected POST request', { status: 405 })
  }

  if (req.headers.get('content-type') !== 'application/json') {
    return new Response('expected json body', { status: 400 })
  }

  // Use Zod to parse and validate the request body
  const parseResult = z.array(jobSchema).safeParse(await req.json())

  if (parseResult.error) {
    return new Response(`invalid request body: ${parseResult.error.message}`, {
      status: 400,
    })
  }

  const pendingJobs = parseResult.data

  // Track jobs that completed successfully
  const completedJobs: Job[] = []

  // Track jobs that failed due to an error
  const failedJobs: FailedJob[] = []

  async function processJobs() {
    let currentJob: Job | undefined

    while ((currentJob = pendingJobs.shift()) !== undefined) {
      try {
        await processJob(currentJob)
        completedJobs.push(currentJob)
      } catch (error) {
        failedJobs.push({
          ...currentJob,
          error: error instanceof Error ? error.message : JSON.stringify(error),
        })
      }
    }
  }

  try {
    // Process jobs while listening for worker termination
    await Promise.race([processJobs(), catchUnload()])
  } catch (error) {
    // If the worker is terminating (e.g. wall clock limit reached),
    // add pending jobs to fail list with termination reason
    failedJobs.push(
      ...pendingJobs.map((job) => ({
        ...job,
        error: error instanceof Error ? error.message : JSON.stringify(error),
      }))
    )
  }

  // Log completed and failed jobs for traceability
  console.log('finished processing jobs:', {
    completedJobs: completedJobs.length,
    failedJobs: failedJobs.length,
  })

  return new Response(
    JSON.stringify({
      completedJobs,
      failedJobs,
    }),
    {
      // 200 OK response
      status: 200,

      // Custom headers to report job status
      headers: {
        'content-type': 'application/json',
        'x-completed-jobs': completedJobs.length.toString(),
        'x-failed-jobs': failedJobs.length.toString(),
      },
    }
  )
})

/**
 * Generates an embedding for the given text.
 */
async function generateEmbedding(text: string) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  })
  const [data] = response.data

  if (!data) {
    throw new Error('failed to generate embedding')
  }

  return data.embedding
}

/**
 * Processes an embedding job.
 */
async function processJob(job: Job) {
  const { jobId, id, schema, table, contentFunction, embeddingColumn } = job

  // Fetch content for the schema/table/row combination
  const [row]: [Row] = await sql`
    select
      id,
      ${sql(contentFunction)}(t) as content
    from
      ${sql(schema)}.${sql(table)} t
    where
      id = ${id}
  `

  if (!row) {
    throw new Error(`row not found: ${schema}.${table}/${id}`)
  }

  if (typeof row.content !== 'string') {
    throw new Error(`invalid content - expected string: ${schema}.${table}/${id}`)
  }

  const embedding = await generateEmbedding(row.content)

  await sql`
    update
      ${sql(schema)}.${sql(table)}
    set
      ${sql(embeddingColumn)} = ${JSON.stringify(embedding)}
    where
      id = ${id}
  `

  await sql`
    select pgmq.delete(${QUEUE_NAME}, ${jobId}::bigint)
  `
}

/**
 * Returns a promise that rejects if the worker is terminating.
 */
function catchUnload() {
  return new Promise((reject) => {
    addEventListener('beforeunload', (ev: any) => {
      reject(new Error(ev.detail?.reason))
    })
  })
}
```

The Edge Function listens for incoming HTTP requests from `pg_net` and processes each embedding job. It is a generic worker that can handle embedding jobs for any table and column. It uses OpenAI's API to generate embeddings and updates the corresponding row in the database. It also deletes the job from the queue once it has been processed.

The function is designed to process multiple jobs independently. If one job fails, it will not affect the processing of other jobs. The function returns a `200 OK` response with a list of completed and failed jobs. We can use this information to diagnose failed jobs. See [Troubleshooting](#troubleshooting) for more details.

You will need to set the `OPENAI_API_KEY` environment variable to authenticate with OpenAI. When running the Edge Function locally, you can add it to a `.env` file:

_.env_:

```
OPENAI_API_KEY=your-api-key
```

When you're ready to deploy the Edge Function, set can set the environment variable using the Supabase CLI:

```shell
supabase secrets set --env-file .env
```

or

```shell
supabase secrets set OPENAI_API_KEY=<your-api-key>
```

Alternatively, you can replace the `generateEmbedding` function with your own embedding generation logic.

See [Deploy to Production](/docs/guides/functions/deploy) for more information on how to deploy the Edge Function.

## Usage

Now that the infrastructure is in place, let's go through an example of how to use this system to automatically generate embeddings for a table of documents. You can use this approach with multiple tables and customize the input for each embedding generation as needed.

### 1. Create table to store documents with embeddings

We'll set up a new `documents` table that will store our content and embeddings:

```sql
-- Table to store documents with embeddings
create table documents (
  id integer primary key generated always as identity,
  title text not null,
  content text not null,
  embedding halfvec(1536),
  created_at timestamp with time zone default now()
);

-- Index for vector search over document embeddings
create index on documents using hnsw (embedding halfvec_cosine_ops);
```

Our `documents` table stores the title and content of each document along with its vector embedding. We use a `halfvec(1536)` column to store the embeddings.

`halfvec` is a `pgvector` data type that stores float values in half precision (16 bits) to save space. Our Edge Function used OpenAI's `text-embedding-3-small` model which generates 1536-dimensional embeddings, so we use the same dimensionality here. Adjust this based on the number of dimensions your embedding model generates.

We use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes) on the vector column. Note that we are choosing `halfvec_cosine_ops` as the index method, which means our future queries will need to use cosine distance (`<=>`) to find similar embeddings. Also note that HNSW indexes support a maximum of 4000 dimensions for `halfvec` vectors, so keep this in mind when choosing an embedding model. If your model generates embeddings with more than 4000 dimensions, you will need to reduce the dimensionality before indexing them. See [Matryoshka embeddings](/blog/matryoshka-embeddings) for a potential solution to shortening dimensions.

Also note that the table must have a primary key column named `id` for our triggers to work correctly with the `util.queue_embeddings` function and for our Edge Function to update the correct row.

### 2. Create triggers to enqueue embedding jobs

Now we'll set up the triggers to enqueue embedding jobs whenever content is inserted or updated:

```sql
-- Customize the input for embedding generation
-- e.g. Concatenate title and content with a markdown header
create or replace function embedding_input(doc documents)
returns text
language plpgsql
immutable
as $$
begin
  return '# ' || doc.title || E'\n\n' || doc.content;
end;
$$;

-- Trigger for insert events
create trigger embed_documents_on_insert
  after insert
  on documents
  for each row
  execute function util.queue_embeddings('embedding_input', 'embedding');

-- Trigger for update events
create trigger embed_documents_on_update
  after update of title, content -- must match the columns in embedding_input()
  on documents
  for each row
  execute function util.queue_embeddings('embedding_input', 'embedding');
```

We create 2 triggers:

1. `embed_documents_on_insert`: Enqueues embedding jobs whenever new rows are inserted into the `documents` table.

2. `embed_documents_on_update`: Enqueues embedding jobs whenever the `title` or `content` columns are updated in the `documents` table.

Both of these triggers use the same `util.queue_embeddings` function that will queue the embedding jobs for processing. They accept 2 arguments:

1. `embedding_input`: The name of the function that generates the input for embedding generation. This function allows you to customize the text input passed to the embedding model (e.g. concatenating the title and content). The function should accept a single row as input and return text.

2. `embedding`: The name of the destination column where the embedding will be stored.

Note that the update trigger only fires when the `title` or `content` columns are updated. This is to avoid unnecessary updates to the embedding column when other columns are updated. Make sure that these columns match the columns used in the `embedding_input` function.

{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}

#### (Optional) Clearing embeddings on update

Note that our trigger will enqueue new embedding jobs when content is updated, but it will not clear any existing embeddings. This means that an embedding can be temporarily out of sync with the content until the new embedding is generated and updated.

If it is more important to have _accurate_ embeddings than _any_ embedding, you can add another trigger to clear the existing embedding until the new one is generated:

```sql
-- Trigger to clear the embedding column on update
create trigger clear_document_embedding_on_update
  before update of title, content -- must match the columns in embedding_input()
  on documents
  for each row
  execute function util.clear_column('embedding');
```

`util.clear_column` is a generic trigger function we created earlier that can be used to clear any column in a table.

- It accepts the column name as an argument. This column must be nullable.
- It requires a `before` trigger with a `for each row` clause.
- It requires the `hstore` extension we created earlier.

This example will clear the `embedding` column whenever the `title` or `content` columns are updated (note the `of title, content` clause). This ensures that the embedding is always in sync with the title and content, but it will result in temporary gaps in search results until the new embedding is generated.

We intentionally use a `before` trigger because it allows us to modify the record before it's written to disk, avoiding an extra `update` statement that would be needed with an `after` trigger.

### 3. Insert and update documents

Let's insert a new document and update its content to see the embedding generation in action:

```sql
-- Insert a new document
insert into documents (title, content)
values
  ('Understanding Vector Databases', 'Vector databases are specialized...');

-- Immediately check the embedding column
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```

You should observe that the `embedding` column is initially `null` after inserting the document. This is because the embedding generation is asynchronous and will be processed by the Edge Function in the next scheduled task.

Wait up to 10 seconds for the next task to run, then check the `embedding` column again:

```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```

You should see the generated embedding for the document.

Next let's update the content of the document:

```sql
-- Update the content of the document
update documents
set content = 'Vector databases allow you to query...'
where title = 'Understanding Vector Databases';

-- Immediately check the embedding column
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```

You should observe that the `embedding` column is reset to `null` after updating the content. This is because of the trigger we added to clear existing embeddings whenever the content is updated. The embedding will be regenerated by the Edge Function in the next scheduled task.

Wait up to 10 seconds for the next task to run, then check the `embedding` column again:

```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases';
```

You should see the updated embedding for the document.

Finally we'll update the title of the document:

```sql
-- Update the title of the document
update documents
set title = 'Understanding Vector Databases with Supabase'
where title = 'Understanding Vector Databases';
```

You should observe that the `embedding` column is once again reset to `null` after updating the title. This is because the trigger we added to clear existing embeddings fires when either the `content` or `title` columns are updated. The embedding will be regenerated by the Edge Function in the next scheduled task.

Wait up to 10 seconds for the next task to run, then check the `embedding` column again:

```sql
select id, embedding
from documents
where title = 'Understanding Vector Databases with Supabase';
```

You should see the updated embedding for the document.

## Troubleshooting

The `embed` Edge Function processes a batch of embedding jobs and returns a `200 OK` response with a list of completed and failed jobs in the body. For example:

```json
{
  "completedJobs": [
    {
      "jobId": "1",
      "id": "1",
      "schema": "public",
      "table": "documents",
      "contentFunction": "embedding_input",
      "embeddingColumn": "embedding"
    }
  ],
  "failedJobs": [
    {
      "jobId": "2",
      "id": "2",
      "schema": "public",
      "table": "documents",
      "contentFunction": "embedding_input",
      "embeddingColumn": "embedding",
      "error": "error connecting to openai api"
    }
  ]
}
```

It also returns the number of completed and failed jobs in the response headers. For example:

```
x-completed-jobs: 1
x-failed-jobs: 1
```

You can also use the `x-deno-execution-id` header to trace the execution of the Edge Function within the [dashboard](/dashboard/project/_/functions) logs.

Each failed job includes an `error` field with a description of the failure. Reasons for a job failing could include:

- An error generating the embedding via external API
- An error connecting to the database
- The edge function being terminated (e.g. due to a wall clock limit)
- Any other error thrown during processing

`pg_net` stores HTTP responses in the `net._http_response` table, which can be queried to diagnose issues with the embedding generation process.

```sql
select
  *
from
  net._http_response
where
  (headers->>'x-failed-jobs')::int > 0;
```

## Conclusion

Automating embedding generation and updates in Postgres allow you to build powerful semantic search capabilities without the complexity of managing embeddings manually.

By combining Postgres features like triggers, queues, and other extensions with Supabase Edge Functions, we can create a robust system that handles embedding generation asynchronously and retries failed jobs automatically.

This system can be customized to work with any content and embedding generation service, providing a flexible and scalable solution for semantic search in Postgres.

## See also

- [What are embeddings?](/docs/guides/ai/concepts)
- [Semantic search](/docs/guides/ai/semantic-search)
- [Vector indexes](/docs/guides/ai/vector-indexes)
- [Supabase Edge Functions](/docs/guides/functions)


path: references/guides/ai/choosing-compute-addon.mdx
content: ---
id: 'ai-choosing-compute-addon'
title: 'Choosing your Compute Add-on'
description: 'Choosing the right Compute Add-on for your vector workload.'
subtitle: 'Choosing the right Compute Add-on for your vector workload.'
sidebar_label: 'Choosing Compute Add-on'
---

You have two options for scaling your vector workload:

1. Increase the size of your database. This guide will help you choose the right size for your workload.
2. Spread your workload across multiple databases. You can find more details about this approach in [Engineering for Scale](engineering-for-scale).

## Dimensionality

The number of dimensions in your embeddings is the most important factor in choosing the right Compute Add-on. In general, the lower the dimensionality the better the performance. We've provided guidance for some of the more common embedding dimensions below. For each benchmark, we used [Vecs](https://github.com/supabase/vecs) to create a collection, upload the embeddings to a single table, and create both the `IVFFlat` and `HNSW` indexes for `inner-product` distance measure for the embedding column. We then ran a series of queries to measure the performance of different compute add-ons:

## HNSW

### 384 dimensions [#hnsw-384-dimensions]

This benchmark uses the dbpedia-entities-openai-1M dataset containing 1,000,000 embeddings of text, regenerated for 384 dimension embeddings. Each embedding is generated using [gte-small](https://huggingface.co/Supabase/gte-small).

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="gte384"
  queryGroup="benchmark"
>
<TabPanel id="gte384" label="gte-small-384">

| Compute Size | Vectors   | m   | ef_construction | ef_search | QPS  | Latency Mean | Latency p95 | RAM Usage  | RAM    |
| ------------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ---------- | ------ |
| Micro        | 100,000   | 16  | 64              | 60        | 580  | 0.017 sec    | 0.024 sec   | 1.2 (Swap) | 1 GB   |
| Small        | 250,000   | 24  | 64              | 60        | 440  | 0.022 sec    | 0.033 sec   | 2 GB       | 2 GB   |
| Medium       | 500,000   | 24  | 64              | 80        | 350  | 0.028 sec    | 0.045 sec   | 4 GB       | 4 GB   |
| Large        | 1,000,000 | 32  | 80              | 100       | 270  | 0.073 sec    | 0.108 sec   | 7 GB       | 8 GB   |
| XL           | 1,000,000 | 32  | 80              | 100       | 525  | 0.038 sec    | 0.059 sec   | 9 GB       | 16 GB  |
| 2XL          | 1,000,000 | 32  | 80              | 100       | 790  | 0.025 sec    | 0.037 sec   | 9 GB       | 32 GB  |
| 4XL          | 1,000,000 | 32  | 80              | 100       | 1650 | 0.015 sec    | 0.018 sec   | 11 GB      | 64 GB  |
| 8XL          | 1,000,000 | 32  | 80              | 100       | 2690 | 0.015 sec    | 0.016 sec   | 13 GB      | 128 GB |
| 12XL         | 1,000,000 | 32  | 80              | 100       | 3900 | 0.014 sec    | 0.016 sec   | 13 GB      | 192 GB |
| 16XL         | 1,000,000 | 32  | 80              | 100       | 4200 | 0.014 sec    | 0.016 sec   | 20 GB      | 256 GB |

Accuracy was 0.99 for benchmarks.

</TabPanel>
</Tabs>

### 960 dimensions [#hnsw-960-dimensions]

This benchmark uses the [gist-960](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="gist960"
  queryGroup="benchmark"
>
<TabPanel id="gist960" label="gist-960">

| Compute Size | Vectors   | m   | ef_construction | ef_search | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 30,000    | 16  | 64              | 65        | 430  | 0.024 sec    | 0.034 sec   | 1.2 GB (Swap) | 1 GB   |
| Small        | 100,000   | 32  | 80              | 60        | 260  | 0.040 sec    | 0.054 sec   | 2.2 GB (Swap) | 2 GB   |
| Medium       | 250,000   | 32  | 80              | 90        | 120  | 0.083 sec    | 0.106 sec   | 4 GB          | 4 GB   |
| Large        | 500,000   | 32  | 80              | 120       | 160  | 0.063 sec    | 0.087 sec   | 7 GB          | 8 GB   |
| XL           | 1,000,000 | 32  | 80              | 200       | 200  | 0.049 sec    | 0.072 sec   | 13 GB         | 16 GB  |
| 2XL          | 1,000,000 | 32  | 80              | 200       | 340  | 0.025 sec    | 0.029 sec   | 17 GB         | 32 GB  |
| 4XL          | 1,000,000 | 32  | 80              | 200       | 630  | 0.031 sec    | 0.050 sec   | 18 GB         | 64 GB  |
| 8XL          | 1,000,000 | 32  | 80              | 200       | 1100 | 0.034 sec    | 0.048 sec   | 19 GB         | 128 GB |
| 12XL         | 1,000,000 | 32  | 80              | 200       | 1420 | 0.041 sec    | 0.095 sec   | 21 GB         | 192 GB |
| 16XL         | 1,000,000 | 32  | 80              | 200       | 1650 | 0.037 sec    | 0.081 sec   | 23 GB         | 256 GB |

Accuracy was 0.99 for benchmarks.

QPS can also be improved by increasing [`m` and `ef_construction`](/docs/guides/ai/going-to-prod#hnsw-understanding-efconstruction--efsearch--and-m). This will allow you to use a smaller value for `ef_search` and increase QPS.

</TabPanel>
</Tabs>

### 1536 dimensions [#hnsw-1536-dimensions]

This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. And 224,482 embeddings from [Wikipedia articles](https://huggingface.co/datasets/Supabase/wikipedia-en-embeddings) for compute add-ons `large` and below. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="openai1536"
  queryGroup="benchmark"
>
<TabPanel id="openai1536" label="OpenAI-1536">

| Compute Size | Vectors   | m   | ef_construction | ef_search | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | --- | --------------- | --------- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 15,000    | 16  | 40              | 40        | 480  | 0.011 sec    | 0.016 sec   | 1.2 GB (Swap) | 1 GB   |
| Small        | 50,000    | 32  | 64              | 100       | 175  | 0.031 sec    | 0.051 sec   | 2.2 GB (Swap) | 2 GB   |
| Medium       | 100,000   | 32  | 64              | 100       | 240  | 0.083 sec    | 0.126 sec   | 4 GB          | 4 GB   |
| Large        | 224,482   | 32  | 64              | 100       | 280  | 0.017 sec    | 0.028 sec   | 8 GB          | 8 GB   |
| XL           | 500,000   | 24  | 56              | 100       | 360  | 0.055 sec    | 0.135 sec   | 13 GB         | 16 GB  |
| 2XL          | 1,000,000 | 24  | 56              | 250       | 560  | 0.036 sec    | 0.058 sec   | 32 GB         | 32 GB  |
| 4XL          | 1,000,000 | 24  | 56              | 250       | 950  | 0.021 sec    | 0.033 sec   | 39 GB         | 64 GB  |
| 8XL          | 1,000,000 | 24  | 56              | 250       | 1650 | 0.016 sec    | 0.023 sec   | 40 GB         | 128 GB |
| 12XL         | 1,000,000 | 24  | 56              | 250       | 1900 | 0.015 sec    | 0.021 sec   | 38 GB         | 192 GB |
| 16XL         | 1,000,000 | 24  | 56              | 250       | 2200 | 0.015 sec    | 0.020 sec   | 40 GB         | 256 GB |

Accuracy was 0.99 for benchmarks.

QPS can also be improved by increasing [`m` and `ef_construction`](/docs/guides/ai/going-to-prod#hnsw-understanding-efconstruction--efsearch--and-m). This will allow you to use a smaller value for `ef_search` and increase QPS. For example, increasing `m` to 32 and `ef_construction` to 80 for 4XL will increase QPS to 1280.

</TabPanel>
</Tabs>

<Admonition type="note">

It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.

</Admonition>

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/instance-type/hnsw-dims--light.png',
    dark: '/docs/img/ai/instance-type/hnsw-dims--dark.png',
  }}
  zoomable
/>

## IVFFlat

### 384 dimensions [#ivfflat-384-dimensions]

This benchmark uses the dbpedia-entities-openai-1M dataset containing 1,000,000 embeddings of text, regenerated for 384 dimension embeddings. Each embedding is generated using [gte-small](https://huggingface.co/Supabase/gte-small).

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="gte384_98"
  queryGroup="benchmark"
>
<TabPanel id="gte384_98" label="gte-small-384, accuracy=.98">

| Compute Size | Vectors   | Lists | Probes | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | ----- | ------ | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 100,000   | 500   | 50     | 205  | 0.048 sec    | 0.066 sec   | 1.2 GB (Swap) | 1 GB   |
| Small        | 250,000   | 1000  | 60     | 160  | 0.062 sec    | 0.079 sec   | 2 GB          | 2 GB   |
| Medium       | 500,000   | 2000  | 80     | 120  | 0.082 sec    | 0.104 sec   | 3.2 GB        | 4 GB   |
| Large        | 1,000,000 | 5000  | 150    | 75   | 0.269 sec    | 0.375 sec   | 6.5 GB        | 8 GB   |
| XL           | 1,000,000 | 5000  | 150    | 150  | 0.131 sec    | 0.178 sec   | 9 GB          | 16 GB  |
| 2XL          | 1,000,000 | 5000  | 150    | 300  | 0.066 sec    | 0.099 sec   | 10 GB         | 32 GB  |
| 4XL          | 1,000,000 | 5000  | 150    | 570  | 0.035 sec    | 0.046 sec   | 10 GB         | 64 GB  |
| 8XL          | 1,000,000 | 5000  | 150    | 1400 | 0.023 sec    | 0.028 sec   | 12 GB         | 128 GB |
| 12XL         | 1,000,000 | 5000  | 150    | 1550 | 0.030 sec    | 0.039 sec   | 12 GB         | 192 GB |
| 16XL         | 1,000,000 | 5000  | 150    | 1800 | 0.030 sec    | 0.039 sec   | 16 GB         | 256 GB |

</TabPanel>
<TabPanel id="gte384_99" label="gte-small-384, accuracy=.99">

| Compute Size | Vectors   | Lists | Probes | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | ----- | ------ | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 100,000   | 500   | 70     | 160  | 0.062 sec    | 0.079 sec   | 1.2 GB (Swap) | 1 GB   |
| Small        | 250,000   | 1000  | 100    | 100  | 0.096 sec    | 0.113 sec   | 2 GB          | 2 GB   |
| Medium       | 500,000   | 2000  | 120    | 85   | 0.117 sec    | 0.147 sec   | 3.2 GB        | 4 GB   |
| Large        | 1,000,000 | 5000  | 250    | 50   | 0.394 sec    | 0.521 sec   | 6.5 GB        | 8 GB   |
| XL           | 1,000,000 | 5000  | 250    | 100  | 0.197 sec    | 0.255 sec   | 10 GB         | 16 GB  |
| 2XL          | 1,000,000 | 5000  | 250    | 200  | 0.098 sec    | 0.140 sec   | 10 GB         | 32 GB  |
| 4XL          | 1,000,000 | 5000  | 250    | 390  | 0.051 sec    | 0.066 sec   | 11 GB         | 64 GB  |
| 8XL          | 1,000,000 | 5000  | 250    | 850  | 0.036 sec    | 0.042 sec   | 12 GB         | 128 GB |
| 12XL         | 1,000,000 | 5000  | 250    | 1000 | 0.043 sec    | 0.055 sec   | 13 GB         | 192 GB |
| 16XL         | 1,000,000 | 5000  | 250    | 1200 | 0.043 sec    | 0.055 sec   | 16 GB         | 256 GB |

</TabPanel>
</Tabs>

### 960 dimensions [#ivfflat-960-dimensions]

This benchmark uses the [gist-960](http://corpus-texmex.irisa.fr/) dataset, which contains 1,000,000 embeddings of images. Each embedding is 960 dimensions.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="gist960"
  queryGroup="benchmark"
>
<TabPanel id="gist960" label="gist-960, probes = 10">

| Compute Size | Vectors   | Lists | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | ----- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 30,000    | 30    | 75   | 0.065 sec    | 0.088 sec   | 1.1 GB (Swap) | 1 GB   |
| Small        | 100,000   | 100   | 78   | 0.064 sec    | 0.092 sec   | 1.8 GB        | 2 GB   |
| Medium       | 250,000   | 250   | 58   | 0.085 sec    | 0.129 sec   | 3.2 GB        | 4 GB   |
| Large        | 500,000   | 500   | 55   | 0.088 sec    | 0.140 sec   | 5 GB          | 8 GB   |
| XL           | 1,000,000 | 1000  | 110  | 0.046 sec    | 0.070 sec   | 14 GB         | 16 GB  |
| 2XL          | 1,000,000 | 1000  | 235  | 0.083 sec    | 0.136 sec   | 10 GB         | 32 GB  |
| 4XL          | 1,000,000 | 1000  | 420  | 0.071 sec    | 0.106 sec   | 11 GB         | 64 GB  |
| 8XL          | 1,000,000 | 1000  | 815  | 0.072 sec    | 0.106 sec   | 13 GB         | 128 GB |
| 12XL         | 1,000,000 | 1000  | 1150 | 0.052 sec    | 0.078 sec   | 15.5 GB       | 192 GB |
| 16XL         | 1,000,000 | 1000  | 1345 | 0.072 sec    | 0.106 sec   | 17.5 GB       | 256 GB |

</TabPanel>
</Tabs>

### 1536 dimensions [#ivfflat-1536-dimensions]

This benchmark uses the [dbpedia-entities-openai-1M](https://huggingface.co/datasets/KShivendu/dbpedia-entities-openai-1M) dataset, which contains 1,000,000 embeddings of text. Each embedding is 1536 dimensions created with the [OpenAI Embeddings API](https://platform.openai.com/docs/guides/embeddings).

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="dbpedia1536"
  queryGroup="benchmark"
>
<TabPanel id="dbpedia1536" label="OpenAI-1536, probes = 10">

| Compute Size | Vectors   | Lists | QPS  | Latency Mean | Latency p95 | RAM Usage     | RAM    |
| ------------ | --------- | ----- | ---- | ------------ | ----------- | ------------- | ------ |
| Micro        | 20,000    | 40    | 135  | 0.372 sec    | 0.412 sec   | 1.2 GB (Swap) | 1 GB   |
| Small        | 50,000    | 100   | 140  | 0.357 sec    | 0.398 sec   | 1.8 GB        | 2 GB   |
| Medium       | 100,000   | 200   | 130  | 0.383 sec    | 0.446 sec   | 3.7 GB        | 4 GB   |
| Large        | 250,000   | 500   | 130  | 0.378 sec    | 0.434 sec   | 7 GB          | 8 GB   |
| XL           | 500,000   | 1000  | 235  | 0.213 sec    | 0.271 sec   | 13.5 GB       | 16 GB  |
| 2XL          | 1,000,000 | 2000  | 380  | 0.133 sec    | 0.236 sec   | 30 GB         | 32 GB  |
| 4XL          | 1,000,000 | 2000  | 720  | 0.068 sec    | 0.120 sec   | 35 GB         | 64 GB  |
| 8XL          | 1,000,000 | 2000  | 1250 | 0.039 sec    | 0.066 sec   | 38 GB         | 128 GB |
| 12XL         | 1,000,000 | 2000  | 1600 | 0.030 sec    | 0.052 sec   | 41 GB         | 192 GB |
| 16XL         | 1,000,000 | 2000  | 1790 | 0.029 sec    | 0.051 sec   | 45 GB         | 256 GB |

For 1,000,000 vectors 10 probes results to accuracy of 0.91. And for 500,000 vectors and below 10 probes results to accuracy in the range of 0.95 - 0.99. To increase accuracy, you need to increase the number of probes.

</TabPanel>
<TabPanel id="dbpedia1536_40" label="OpenAI-1536, probes = 40">

| Compute Size | Vectors   | Lists | QPS | Latency Mean | Latency p95 | RAM Usage | RAM    |
| ------------ | --------- | ----- | --- | ------------ | ----------- | --------- | ------ |
| Micro        | 20,000    | 40    | -   | -            | -           | -         | 1 GB   |
| Small        | 50,000    | 100   | -   | -            | -           | -         | 2 GB   |
| Medium       | 100,000   | 200   | -   | -            | -           | -         | 4 GB   |
| Large        | 250,000   | 500   | -   | -            | -           | -         | 8 GB   |
| XL           | 500,000   | 1000  | -   | -            | -           | -         | 16 GB  |
| 2XL          | 1,000,000 | 2000  | 140 | 0.358 sec    | 0.575 sec   | 30 GB     | 32 GB  |
| 4XL          | 1,000,000 | 2000  | 270 | 0.186 sec    | 0.304 sec   | 35 GB     | 64 GB  |
| 8XL          | 1,000,000 | 2000  | 470 | 0.104 sec    | 0.166 sec   | 38 GB     | 128 GB |
| 12XL         | 1,000,000 | 2000  | 600 | 0.085 sec    | 0.132 sec   | 41 GB     | 192 GB |
| 16XL         | 1,000,000 | 2000  | 670 | 0.081 sec    | 0.129 sec   | 45 GB     | 256 GB |

For 1,000,000 vectors 40 probes results to accuracy of 0.98. Note that exact values may vary depending on the dataset and queries, we recommend to run benchmarks with your own data to get precise results. Use this table as a reference.

</TabPanel>
</Tabs>

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/going-prod/size-to-rps--light.png',
    dark: '/docs/img/ai/going-prod/size-to-rps--dark.png',
  }}
  zoomable
/>

<Admonition type="note">

It is possible to upload more vectors to a single table if Memory allows it (for example, 4XL plan and higher for OpenAI embeddings). But it will affect the performance of the queries: QPS will be lower, and latency will be higher. Scaling should be almost linear, but it is recommended to benchmark your workload to find the optimal number of vectors per table and per database instance.

</Admonition>

## Performance tips

There are various ways to improve your pgvector performance. Here are some tips:

### Pre-warming your database

It's useful to execute a few thousand “warm-up” queries before going into production. This helps help with RAM utilization. This can also help to determine that you've selected the right compute size for your workload.

### Fine-tune index parameters

You can increase the Requests per Second by increasing `m` and `ef_construction` or `lists`. This also has an important caveat: building the index takes longer with higher values for these parameters.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="hnsw"
  queryGroup="index-type"
>
<TabPanel id="hnsw" label="HNSW">

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png',
    dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png',
  }}
  zoomable
/>

</TabPanel>
<TabPanel id="ivfflat" label="IVFFlat">

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/instance-type/lists-for-1m--light.png',
    dark: '/docs/img/ai/instance-type/lists-for-1m--dark.png',
  }}
  zoomable
/>

</TabPanel>
</Tabs>

Check out more tips and the complete step-by-step guide in [Going to Production for AI applications](going-to-prod).

## Benchmark methodology

We follow techniques outlined in the [ANN Benchmarks](https://github.com/erikbern/ann-benchmarks) methodology. A Python test runner is responsible for uploading the data, creating the index, and running the queries. The pgvector engine is implemented using [vecs](https://github.com/supabase/vecs), a Python client for pgvector.

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/instance-type/vecs-benchmark--light.png',
    dark: '/docs/img/ai/instance-type/vecs-benchmark--dark.png',
  }}
  className="max-h-[650px]"
  zoomable
/>

Each test is run for a minimum of 30-40 minutes. They include a series of experiments executed at different concurrency levels to measure the engine's performance under different load types. The results are then averaged.

As a general recommendation, we suggest using a concurrency level of 5 or more for most workloads and 30 or more for high-load workloads.


path: references/guides/ai/concepts.mdx
content: ---
id: 'ai-concepts'
title: 'Concepts'
description: 'Learn about embeddings within AI and vector applications.'
sidebar_label: 'Concepts'
---

Embeddings are core to many AI and vector applications. This guide covers these concepts. If you prefer to get started right away, see our guide on [Generating Embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).

## What are embeddings?

Embeddings capture the "relatedness" of text, images, video, or other types of information. This relatedness is most commonly used for:

- **Search:** how similar is a search term to a body of text?
- **Recommendations:** how similar are two products?
- **Classifications:** how do we categorize a body of text?
- **Clustering:** how do we identify trends?

Let's explore an example of text embeddings. Say we have three phrases:

1. "The cat chases the mouse"
2. "The kitten hunts rodents"
   {/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
3. "I like ham sandwiches"

Your job is to group phrases with similar meaning. If you are a human, this should be obvious. Phrases 1 and 2 are almost identical, while phrase 3 has a completely different meaning.

Although phrases 1 and 2 are similar, they share no common vocabulary (besides "the"). Yet their meanings are nearly identical. How can we teach a computer that these are the same?

## Human language

Humans use words and symbols to communicate language. But words in isolation are mostly meaningless - we need to draw from shared knowledge & experience in order to make sense of them. The phrase “You should Google it” only makes sense if you know that Google is a search engine and that people have been using it as a verb.

In the same way, we need to train a neural network model to understand human language. An effective model should be trained on millions of different examples to understand what each word, phrase, sentence, or paragraph could mean in different contexts.

So how does this relate to embeddings?

## How do embeddings work?

Embeddings compress discrete information (words & symbols) into distributed continuous-valued data (vectors). If we took our phrases from before and plot them on a chart, it might look something like this:

<img src="/docs/img/ai/vector-similarity.png" alt="Vector similarity" width="640" height="640" />

Phrases 1 and 2 would be plotted close to each other, since their meanings are similar. We would expect phrase 3 to live somewhere far away since it isn't related. If we had a fourth phrase, “Sally ate Swiss cheese”, this might exist somewhere between phrase 3 (cheese can go on sandwiches) and phrase 1 (mice like Swiss cheese).

In this example we only have 2 dimensions: the X and Y axis. In reality, we would need many more dimensions to effectively capture the complexities of human language.

## Using embeddings

Compared to our 2-dimensional example above, most embedding models will output many more dimensions. For example the open source [`gte-small`](https://huggingface.co/Supabase/gte-small) model outputs 384 dimensions.

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
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)


path: references/guides/ai/engineering-for-scale.mdx
content: ---
id: 'ai-engineering-for-scale'
title: 'Engineering for Scale'
description: 'Building an enterprise-grade vector architecture'
subtitle: 'Building an enterprise-grade vector architecture.'
sidebar_label: 'Engineering for Scale'
---

Content sources for vectors can be extremely large. As you grow you should run your Vector workloads across several secondary databases (sometimes called "pods"), which allows each collection to scale independently.

## Simple workloads

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):

<Image
  alt="single database"
  src={{
    light: '/docs/img/ai/scaling/engineering-for-scale--single-database--light.png',
    dark: '/docs/img/ai/scaling/engineering-for-scale--single-database--dark.png',
  }}
  zoomable
/>

For example, with 3 collections, called `docs`, `posts`, and `images`, we could expose the "docs" inside the public schema like this:

```sql
create view public.docs as
select
  id,
  embedding,
  metadata, # Expose the metadata as JSON
  (metadata->>'url')::text as url # Extract the URL as a string
from vector
```

You can then use any of the client libraries to access your collections within your applications:

{/* prettier-ignore */}
```js
const { data, error } = await supabase
  .from('docs')
  .select('id, embedding, metadata')
  .eq('url', '/hello-world')
```

## Enterprise workloads

As you move into production, we recommend 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.

<Image
  alt="With secondaries"
  src={{
    light: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--light.png',
    dark: '/docs/img/ai/scaling/engineering-for-scale--with-secondaries--dark.png',
  }}
  zoomable
/>

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:

1. Query the collections directly using Vecs.
2. Access the collections from your Primary database through a Wrapper.

You can use both of these in tandem to suit your use-case. We recommend option `1` wherever possible, as it offers the most scalability.

### Query collections using Vecs

Vecs provides methods for querying collections, either using a [cosine similarity function](https://supabase.github.io/vecs/api/#basic) or with [metadata filtering](https://supabase.github.io/vecs/api/#metadata-filtering).

```python
# cosine similarity
docs.query(query_vector=[0.4,0.5,0.6], limit=5)

# metadata filtering
docs.query(
    query_vector=[0.4,0.5,0.6],
    limit=5,
    filters={"year": {"$eq": 2012}}, # metadata filters
)
```

### Accessing external collections using Wrappers

Supabase supports [Foreign Data Wrappers](/blog/postgres-foreign-data-wrappers-rust). Wrappers allow you to connect two databases together so that you can query them over the network.

This involves 2 steps: connecting to your remote database from the primary and creating a Foreign Table.

#### Connecting your remote database

Inside your Primary database we need to provide the credentials to access the secondary database:

```sql
create extension postgres_fdw;

create server docs_server
foreign data wrapper postgres_fdw
options (host 'db.xxx.supabase.co', port '5432', dbname 'postgres');

create user mapping for docs_user
server docs_server
options (user 'postgres', password 'password');
```

#### Create a foreign table

We can now create a foreign table to access the data in our secondary project.

```sql
create foreign table docs (
  id text not null,
  embedding extensions.vector(384),
  metadata jsonb,
  url text
)
server docs_server
options (schema_name 'public', table_name 'docs');
```

This looks very similar to our View example above, and you can continue to use the client libraries to access your collections through the foreign table:

{/* prettier-ignore */}
```js
const { data, error } = await supabase
  .from('docs')
  .select('id, embedding, metadata')
  .eq('url', '/hello-world')
```

### Enterprise architecture

This diagram provides an example architecture that allows 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 only show one):

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/scaling/engineering-for-scale--multi-database--light.png',
    dark: '/docs/img/ai/scaling/engineering-for-scale--multi-database--dark.png',
  }}
  zoomable
/>


path: references/guides/ai/examples/building-chatgpt-plugins.mdx
content: ---
title: 'Building ChatGPT plugins'
subtitle: 'Use Supabase as a Retrieval Store for your ChatGPT plugin.'
breadcrumb: 'AI Examples'
---

ChatGPT recently released [Plugins](https://openai.com/blog/chatgpt-plugins) which help ChatGPT access up-to-date information, run computations, or use third-party services.
If you're building a plugin for ChatGPT, you'll probably want to answer questions from a specific source. We can solve this with “retrieval plugins”, which allow ChatGPT to access information from a database.

## What is ChatGPT Retrieval Plugin?

A [Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) is a Python project designed to inject external data into a ChatGPT conversation. It does a few things:

1. Turn documents into smaller chunks.
2. Converts chunks into embeddings using OpenAI's `text-embedding-ada-002` model.
3. Stores the embeddings into a vector database.
4. Queries the vector database for relevant documents when a question is asked.

It allows ChatGPT to dynamically pull relevant information into conversations from your data sources. This could be PDF documents, Confluence, or Notion knowledge bases.

## Example: Chat with Postgres docs

Let’s build an example where we can “ask ChatGPT questions” about the Postgres documentation. Although ChatGPT already knows about the Postgres documentation because it is publicly available, this is a simple example which demonstrates how to work with PDF files.

This plugin requires several steps:

1. Download all the [Postgres docs as a PDF](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf)
2. Convert the docs into chunks of embedded text and store them in Supabase
3. Run our plugin locally so that we can ask questions about the Postgres docs.

We'll be saving the Postgres documentation in Postgres, and ChatGPT will be retrieving the documentation whenever a user asks a question:

<Image
  alt="diagram reference"
  className="max-h-[600px]"
  src={{
    light: '/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--light.png',
    dark: '/docs/img/ai/chatgpt-plugins/chatgpt-plugin-scheme--dark.png',
  }}
  zoomable
/>

### Step 1: Fork the ChatGPT Retrieval Plugin repository

Fork the ChatGPT Retrieval Plugin repository to your GitHub account and clone it to your local machine. Read through the `README.md` file to understand the project structure.

### Step 2: Install dependencies

Choose your desired datastore provider and remove unused dependencies from `pyproject.toml`. For this example, we'll use Supabase. And install dependencies with Poetry:

```bash
poetry install
```

### Step 3: Create a Supabase project

Create a [Supabase project](/dashboard) and database by following the instructions [here](/docs/guides/platform). Export the environment variables required for the retrieval plugin to work:

```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=supabase
export SUPABASE_URL=<supabase_url>
export SUPABASE_SERVICE_ROLE_KEY=<supabase_key>
```

For Postgres datastore, you'll need to export these environment variables instead:

```bash
export OPENAI_API_KEY=<open_ai_api_key>
export DATASTORE=postgres
export PG_HOST=<postgres_host_url>
export PG_PASSWORD=<postgres_password>
```

### Step 4: Run Postgres locally

To start quicker you may use Supabase CLI to spin everything up locally as it already includes pgvector from the start. Install `supabase-cli`, go to the `examples/providers` folder in the repo and run:

```bash
supabase start
```

This will pull all docker images and run Supabase stack in docker on your local machine. It will also apply all the necessary migrations to set the whole thing up. You can then use your local setup the same way, just export the environment variables and follow to the next steps.

Using `supabase-cli` is not required and you can use any other docker image or hosted version of Postgres that includes `pgvector`. Just make sure you run migrations from `examples/providers/supabase/migrations/20230414142107_init_pg_vector.sql`.

### Step 5: Obtain OpenAI API key

To create embeddings Plugin uses OpenAI API and `text-embedding-ada-002` model. Each time we add some data to our datastore, or try to query relevant information from it, embedding will be created either for inserted data chunk, or for the query itself. To make it work we need to export `OPENAI_API_KEY`. If you already have an account in OpenAI, you just need to go to [User Settings - API keys](https://platform.openai.com/account/api-keys) and Create new secret key.

![OpenAI Secret Keys](/docs/img/ai/chatgpt-plugins/openai-secret-keys.png)

### Step 6: Run the plugin

Execute the following command to run the plugin:

```bash
poetry run dev
# output
INFO:     Will watch for changes in these directories: ['./chatgpt-retrieval-plugin']
INFO:     Uvicorn running on http://localhost:3333 (Press CTRL+C to quit)
INFO:     Started reloader process [87843] using WatchFiles
INFO:     Started server process [87849]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
```

The plugin will start on your localhost - port `:3333` by default.

### Step 6: Populating data in the datastore

For this example, we'll upload Postgres documentation to the datastore. Download the [Postgres documentation](https://www.postgresql.org/files/documentation/pdf/15/postgresql-15-US.pdf) and use the `/upsert-file` endpoint to upload it:

```bash
curl -X POST -F \\"file=@./postgresql-15-US.pdf\\" <http://localhost:3333/upsert-file>
```

The plugin will split your data and documents into smaller chunks automatically. You can view the chunks using the Supabase dashboard or any other SQL client you prefer. The entire Postgres Documentation yielded 7,904 records, which is not a lot, but we can try to add index for `embedding` column to speed things up by a little. To do so, you should run the following SQL command:

```sql
create index on documents
using hnsw (embedding vector_ip_ops)
with (lists = 10);
```

This will create an index for the inner product distance function. Important to note that it is an approximate index. It will change the logic from performing the exact nearest neighbor search to the approximate nearest neighbor search.

We are using `lists = 10`, because as a general guideline, you should start looking for optimal lists constant value with the formula: `rows / 1000` when you have less than 1 million records in your table.

### Step 7: Using our plugin within ChatGPT

To integrate our plugin with ChatGPT, register it in the ChatGPT dashboard. Assuming you have access to ChatGPT Plugins and plugin development, select the Plugins model in a new chat, then choose "Plugin store" and "Develop your own plugin." Enter `localhost:3333` into the domain input, and your plugin is now part of ChatGPT.

![ChatGPT Plugin Store](/docs/img/ai/chatgpt-plugins/chatgpt-plugin-store.png)

![ChatGPT Local Plugin](/docs/img/ai/chatgpt-plugins/chatgpt-local-plugin.png)

You can now ask questions about Postgres and receive answers derived from the documentation.

Let's try it out: ask ChatGPT to find out when to use `check` and when to use `using`. You will be able to see what queries were sent to our plugin and what it responded to.

![Ask ChatGPT](/docs/img/ai/chatgpt-plugins/ask-chatgpt.png)

And after ChatGPT receives a response from the plugin it will answer your question with the data from the documentation.

![ChatGPT Reply](/docs/img/ai/chatgpt-plugins/chatgpt-reply.png)

## Resources

- ChatGPT Retrieval Plugin: [github.com/openai/chatgpt-retrieval-plugin](https://github.com/openai/chatgpt-retrieval-plugin)
- ChatGPT Plugins: [official documentation](https://platform.openai.com/docs/plugins/introduction)


path: references/guides/ai/examples/headless-vector-search.mdx
content: ---
title: 'Adding generative Q&A for your documentation'
subtitle: 'Learn how to build a ChatGPT-style doc search powered using our headless search toolkit.'
breadcrumb: 'AI Examples'
---

Supabase provides a [Headless Search Toolkit](https://github.com/supabase/headless-vector-search) for adding "Generative Q&A" to your documentation. The toolkit is "headless", so that you can integrate it into your existing website and style it to match your website theme.

You can see how this works with the Supabase docs. Just hit `cmd+k` and "ask" for something like "what are the features of Supabase?". You will see that the response is streamed back, using the information provided in the docs:

![headless search](/docs/img/ai/headless-search/headless.png)

## Tech stack

- Supabase: Database & Edge Functions.
- OpenAI: Embeddings and completions.
- GitHub Actions: for ingesting your markdown docs.

## Toolkit

This toolkit consists of 2 parts:

- The [Headless Vector Search](https://github.com/supabase/headless-vector-search) template which you can deploy in your own organization.
- A [GitHub Action](https://github.com/supabase/embeddings-generator) which will ingest your markdown files, convert them to embeddings, and store them in your database.

## Usage

There are 3 steps to build similarity search inside your documentation:

1. Prepare your database.
2. Ingest your documentation.
3. Add a search interface.

### Prepare your database

To prepare, create a [new Supabase project](https://database.new) and store the database and API credentials, which you can find in the project [settings](/dashboard/project/_/settings).

Now we can use the [Headless Vector Search](https://github.com/supabase/headless-vector-search#set-up) instructions to set up the database:

1. Clone the repo to your local machine: `git clone [REDACTED_SECRET]:supabase/headless-vector-search.git`
2. Link the repo to your remote project: `supabase link --project-ref XXX`
3. Apply the database migrations: `supabase db push`
4. Set your OpenAI key as a secret: `supabase secrets set OPENAI_API_KEY=sk-xxx`
5. Deploy the Edge Functions: `supabase functions deploy --no-verify-jwt`
6. Expose `docs` schema via API in Supabase Dashboard [settings](/dashboard/project/_/settings/api) > `API Settings` > `Exposed schemas`

### Ingest your documentation

Now we need to push your documentation into the database as embeddings. You can do this manually, but to make it easier we've created a [GitHub Action](https://github.com/marketplace/actions/supabase-embeddings-generator) which can update your database every time there is a Pull Request.

In your knowledge base repository, create a new action called `.github/workflows/generate_embeddings.yml` with the following content:

```yml
name: 'generate_embeddings'
on: # run on main branch changes
  push:
    branches:
      - main

jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: supabase/embeddings-generator@v0.0.x # Update this to the latest version.
        with:
          supabase-url: 'https://your-project-ref.supabase.co' # Update this to your project URL.
          supabase-service-role-key: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
          openai-key: ${{ secrets.OPENAI_API_KEY }}
          docs-root-path: 'docs' # the path to the root of your md(x) files
```

Make sure to choose the latest version, and set your `SUPABASE_SERVICE_ROLE_KEY` and `OPENAI_API_KEY` as repository secrets in your repo settings (settings > secrets > actions).

### Add a search interface

Now inside your docs, you need to create a search interface. Because this is a headless interface, you can use it with any language. The only requirement is that you send the user query to the `query` Edge Function, which will stream an answer back from OpenAI. It might look something like this:

```js
const onSubmit = (e: Event) => {
  e.preventDefault()
  answer.value = ""
  isLoading.value = true

  const query = new URLSearchParams({ query: inputRef.current!.value })
  const projectUrl = `https://your-project-ref.supabase.co/functions/v1`
  const queryURL = `${projectURL}/${query}`
  const eventSource = new EventSource(queryURL)

  eventSource.addEventListener("error", (err) => {
    isLoading.value = false
    console.error(err)
  })

  eventSource.addEventListener("message", (e: MessageEvent) => {
    isLoading.value = false

    if (e.data === "[DONE]") {
      eventSource.close()
      return
    }

    const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
    const text = completionResponse.choices[0].text

    answer.value += text
  });

  isLoading.value = true
}
```

## Resources

- Read about how we built [ChatGPT for the Supabase Docs](/blog/chatgpt-supabase-docs).
- Read the pgvector Docs for [Embeddings and vector similarity](/docs/guides/database/extensions/pgvector)
- See how to build something like this from scratch [using Next.js](/docs/guides/ai/examples/nextjs-vector-search).


path: references/guides/ai/examples/huggingface-image-captioning.mdx
content: ---
title: 'Generate image captions using Hugging Face'
description: 'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.'
subtitle: 'Use the Hugging Face Inference API to make calls to 100,000+ Machine Learning models from Supabase Edge Functions.'
video: 'https://www.youtube.com/v/OgnYxRkxEUw'
tocVideo: 'OgnYxRkxEUw'
---

We can combine Hugging Face with [Supabase Storage](/storage) and [Database Webhooks](/docs/guides/database/webhooks) to automatically caption for any image we upload to a storage bucket.

## About Hugging Face

[Hugging Face](https://huggingface.co/) is the collaboration platform for the machine learning community.

[Huggingface.js](https://huggingface.co/docs/huggingface.js/index) provides a convenient way to make calls to 100,000+ Machine Learning models, making it easy to incorporate AI functionality into your [Supabase Edge Functions](/edge-functions).

## Setup

- Open your Supabase project dashboard or [create a new project](/dashboard/projects).
- [Create a new bucket](/dashboard/project/_/storage/buckets) called `images`.
- Generate TypeScript types from remote Database.
- Create a new Database table called `image_caption`.
  - Create `id` column of type `uuid` which references `storage.objects.id`.
  - Create a `caption` column of type `text`.
- Regenerate TypeScript types to include new `image_caption` table.
- Deploy the function to Supabase: `supabase functions deploy huggingface-image-captioning`.
- Create the Database Webhook in the [Supabase Dashboard](/dashboard/project/_/database/hooks) to trigger the `huggingface-image-captioning` function anytime a record is added to the `storage.objects` table.

## Generate TypeScript types

To generate the types.ts file for the storage and public schemas, run the following command in the terminal:

```bash
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
```

## Code

Find the complete code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/huggingface-image-captioning).

```ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'
import { createClient } from 'npm:@supabase/supabase-js@2'
import { Database } from './types.ts'

console.log('Hello from `huggingface-image-captioning` function!')

const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))

type SoRecord = Database['storage']['Tables']['objects']['Row']
interface WebhookPayload {
  type: 'INSERT' | 'UPDATE' | 'DELETE'
  table: string
  record: SoRecord
  schema: 'public'
  old_record: null | SoRecord
}

serve(async (req) => {
  const payload: WebhookPayload = await req.json()
  const soRecord = payload.record
  const supabaseAdminClient = createClient<Database>(
    // Supabase API URL - env var exported by default when deployed.
    Deno.env.get('SUPABASE_URL') ?? '',
    // Supabase API SERVICE ROLE KEY - env var exported by default when deployed.
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  )

  // Construct image url from storage
  const { data, error } = await supabaseAdminClient.storage
    .from(soRecord.bucket_id!)
    .createSignedUrl(soRecord.path_tokens!.join('/'), 60)
  if (error) throw error
  const { signedUrl } = data

  // Run image captioning with Huggingface
  const imgDesc = await hf.imageToText({
    data: await (await fetch(signedUrl)).blob(),
    model: 'nlpconnect/vit-gpt2-image-captioning',
  })

  // Store image caption in Database table
  await supabaseAdminClient
    .from('image_caption')
    .insert({ id: soRecord.id!, caption: imgDesc.generated_text })
    .throwOnError()

  return new Response('ok')
})
```


path: references/guides/ai/examples/image-search-openai-clip.mdx
content: ---
id: 'examples-image-search-python'
title: 'Image Search with OpenAI CLIP'
description: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.'
subtitle: 'Implement image search with the OpenAI CLIP Model and Supabase Vector.'
---

The [OpenAI CLIP Model](https://github.com/openai/CLIP) was trained on a variety of (image, text)-pairs. You can use the CLIP model for:

- Text-to-Image / Image-To-Text / Image-to-Image / Text-to-Text Search
- You can fine-tune it on your own image and text data with the regular `SentenceTransformers` training code.

[`SentenceTransformers`](https://www.sbert.net/examples/applications/image-search/README.html) provides models that allow you to embed images and text into the same vector space. You can use this to find similar images as well as to implement image search.

You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search#image-search-with-supabase-vector).

## Create a new Python project with Poetry

[Poetry](https://python-poetry.org/) provides packaging and dependency management for Python. If you haven't already, install poetry via pip:

```shell
pip install poetry
```

Then initialize a new project:

```shell
poetry new image-search
```

## Setup Supabase project

If you haven't already, [install the Supabase CLI](/docs/guides/cli), then initialize Supabase in the root of your newly created poetry project:

```shell
supabase init
```

Next, start your local Supabase stack:

```shell
supabase start
```

This will start up the Supabase stack locally and print out a bunch of environment details, including your local `DB URL`. Make a note of that for later user.

## Install the dependencies

We will need to add the following dependencies to our project:

- [`vecs`](https://github.com/supabase/vecs#vecs): Supabase Vector Python Client.
- [`sentence-transformers`](https://huggingface.co/sentence-transformers/clip-ViT-B-32): a framework for sentence, text and image embeddings (used with OpenAI CLIP model)
- [`matplotlib`](https://matplotlib.org/): for displaying our image result

```shell
poetry add vecs sentence-transformers matplotlib
```

## Import the necessary dependencies

At the top of your main python script, import the dependencies and store your `DB URL` from above in a variable:

```python
from PIL import Image
from sentence_transformers import SentenceTransformer
import vecs
from matplotlib import pyplot as plt
from matplotlib import image as mpimg

DB_CONNECTION = "postgresql://postgres:postgres@localhost:54322/postgres"
```

## Create embeddings for your images

In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/image_search/images) or you can find license free images on [Unsplash](https://unsplash.com).

Next, create a `seed` method, which will create a new Supabase Vector Collection, generate embeddings for your images, and upsert the embeddings into your database:

```python
def seed():
    # create vector store client
    vx = vecs.create_client(DB_CONNECTION)

    # create a collection of vectors with 3 dimensions
    images = vx.get_or_create_collection(name="image_vectors", dimension=512)

    # Load CLIP model
    model = SentenceTransformer('clip-ViT-B-32')

    # Encode an image:
    img_emb1 = model.encode(Image.open('./images/one.jpg'))
    img_emb2 = model.encode(Image.open('./images/two.jpg'))
    img_emb3 = model.encode(Image.open('./images/three.jpg'))
    img_emb4 = model.encode(Image.open('./images/four.jpg'))

    # add records to the *images* collection
    images.upsert(
        records=[
            (
                "one.jpg",        # the vector's identifier
                img_emb1,          # the vector. list or np.array
                {"type": "jpg"}   # associated  metadata
            ), (
                "two.jpg",
                img_emb2,
                {"type": "jpg"}
            ), (
                "three.jpg",
                img_emb3,
                {"type": "jpg"}
            ), (
                "four.jpg",
                img_emb4,
                {"type": "jpg"}
            )
        ]
    )
    print("Inserted images")

    # index the collection for fast search performance
    images.create_index()
    print("Created index")
```

Add this method as a script in your `pyproject.toml` file:

```toml
[tool.poetry.scripts]
seed = "image_search.main:seed"
search = "image_search.main:search"
```

After activating the virtual environment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor), selecting the `vecs` schema, and the `image_vectors` database.

## Perform an image search from a text query

With Supabase Vector we can query our embeddings. We can use either an image as search input or alternative we can generate an embedding from a string input and use that as the query input:

```python
def search():
    # create vector store client
    vx = vecs.create_client(DB_CONNECTION)
    images = vx.get_or_create_collection(name="image_vectors", dimension=512)

    # Load CLIP model
    model = SentenceTransformer('clip-ViT-B-32')
    # Encode text query
    query_string = "a bike in front of a red brick wall"
    text_emb = model.encode(query_string)

    # query the collection filtering metadata for "type" = "jpg"
    results = images.query(
        data=text_emb,                      # required
        limit=1,                            # number of records to return
        filters={"type": {"$eq": "jpg"}},   # metadata filters
    )
    result = results[0]
    print(result)
    plt.title(result)
    image = mpimg.imread('./images/' + result)
    plt.imshow(image)
    plt.show()
```

By limiting the query to one result, we can show the most relevant image to the user. Finally we use `matplotlib` to show the image result to the user.

Go ahead and test it out by running `poetry run search` and you will be presented with an image of a "bike in front of a red brick wall".

## Conclusion

With just a couple of lines of Python you are able to implement image search as well as reverse image search using OpenAI's CLIP model and Supabase Vector.


path: references/guides/ai/examples/mixpeek-video-search.mdx
content: ---
id: 'examples-video-search-mixpeek'
title: 'Video Search with Mixpeek Multimodal Embeddings'
description: 'Implement video search with the Mixpeek Multimodal Embed API and Supabase Vector.'
subtitle: 'Implement video search with the Mixpeek Multimodal Embed API and Supabase Vector.'
---

The [Mixpeek Embed API](https://docs.mixpeek.com/api-documentation/inference/embed) allows you to generate embeddings for various types of content, including videos and text. You can use these embeddings for:

- Text-to-Video / Video-To-Text / Video-to-Video / Text-to-Text Search
- Fine-tuning on your own video and text data

This guide demonstrates how to implement video search using Mixpeek Embed for video processing and embedding, and Supabase Vector for storing and querying embeddings.

## Create a new Python project with Poetry

[Poetry](https://python-poetry.org/) provides packaging and dependency management for Python. If you haven't already, install poetry via pip:

```shell
pip install poetry
```

Then initialize a new project:

```shell
poetry new video-search
```

## Setup Supabase project

If you haven't already, [install the Supabase CLI](/docs/guides/cli), then initialize Supabase in the root of your newly created poetry project:

```shell
supabase init
```

Next, start your local Supabase stack:

```shell
supabase start
```

This will start up the Supabase stack locally and print out a bunch of environment details, including your local `DB URL`. Make a note of that for later use.

## Install the dependencies

Add the following dependencies to your project:

- [`supabase`](https://github.com/supabase-community/supabase-py): Supabase Python Client
- [`mixpeek`](https://github.com/mixpeek/python-sdk): Mixpeek Python Client for embedding generation

```shell
poetry add supabase mixpeek
```

## Import the necessary dependencies

At the top of your main Python script, import the dependencies and store your environment variables:

```python
from supabase import create_client, Client
from mixpeek import Mixpeek
import os

SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_API_KEY")
MIXPEEK_API_KEY = os.getenv("MIXPEEK_API_KEY")
```

## Create embeddings for your videos

Next, create a `seed` method, which will create a new Supabase table, generate embeddings for your video chunks, and insert the embeddings into your database:

```python
def seed():
    # Initialize Supabase and Mixpeek clients
    supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
    mixpeek = Mixpeek(MIXPEEK_API_KEY)

    # Create a table for storing video chunk embeddings
    supabase.table("video_chunks").create({
        "id": "text",
        "start_time": "float8",
        "end_time": "float8",
        "embedding": "extensions.vector(768)",
        "metadata": "jsonb"
    })

    # Process and embed video
    video_url = "https://example.com/your_video.mp4"
    processed_chunks = mixpeek.tools.video.process(
        video_source=video_url,
        chunk_interval=1,  # 1 second intervals
        resolution=[720, 1280]
    )

    for chunk in processed_chunks:
        print(f"Processing video chunk: {chunk['start_time']}")

        # Generate embedding using Mixpeek
        embed_response = mixpeek.embed.video(
            model_id="vuse-generic-v1",
            input=chunk['base64_chunk'],
            input_type="base64"
        )

        # Insert into Supabase
        supabase.table("video_chunks").insert({
            "id": f"chunk_{chunk['start_time']}",
            "start_time": chunk["start_time"],
            "end_time": chunk["end_time"],
            "embedding": embed_response['embedding'],
            "metadata": {"video_url": video_url}
        }).execute()

    print("Video processed and embeddings inserted")

    # Create index for fast search performance
    supabase.query("CREATE INDEX ON video_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)").execute()
    print("Created index")
```

Add this method as a script in your `pyproject.toml` file:

```toml
[tool.poetry.scripts]
seed = "video_search.main:seed"
search = "video_search.main:search"
```

After activating the virtual environment with `poetry shell`, you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your local database by visiting the local Supabase dashboard at [localhost:54323](http://localhost:54323/project/default/editor).

## Perform a video search from a text query

With Supabase Vector, you can query your embeddings. You can use either a video clip as search input or alternatively, you can generate an embedding from a string input and use that as the query input:

```python
def search():
    # Initialize Supabase and Mixpeek clients
    supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
    mixpeek = Mixpeek(MIXPEEK_API_KEY)

    # Generate embedding for text query
    query_string = "a car chase scene"
    text_emb = mixpeek.embed.video(
        model_id="vuse-generic-v1",
        input=query_string,
        input_type="text"
    )

    # Query the collection
    results = supabase.rpc(
        'match_video_chunks',
        {
            'query_embedding': text_emb['embedding'],
            'match_threshold': 0.8,
            'match_count': 5
        }
    ).execute()

    # Display the results
    if results.data:
        for result in results.data:
            print(f"Matched chunk from {result['start_time']} to {result['end_time']} seconds")
            print(f"Video URL: {result['metadata']['video_url']}")
            print(f"Similarity: {result['similarity']}")
            print("---")
    else:
        print("No matching video chunks found")
```

This query will return the top 5 most similar video chunks from your database.

You can now test it out by running `poetry run search`, and you will be presented with the most relevant video chunks to the query "a car chase scene".

## Conclusion

With just a couple of Python scripts, you are able to implement video search as well as reverse video search using Mixpeek Embed and Supabase Vector. This approach allows for powerful semantic search capabilities that can be integrated into various applications, enabling you to search through video content using both text and video queries.


path: references/guides/ai/examples/nextjs-vector-search.mdx
content: ---
title: 'Vector search with Next.js and OpenAI'
subtitle: 'Learn how to build a ChatGPT-style doc search powered by Next.js, OpenAI, and Supabase.'
breadcrumb: 'AI Examples'
video: 'https://www.youtube.com/v/xmfNUCjszh4'
tocVideo: 'xmfNUCjszh4'
---

While our [Headless Vector search](/docs/guides/ai/examples/headless-vector-search) provides a toolkit for generative Q&A, in this tutorial we'll go more in-depth, build a custom ChatGPT-like search experience from the ground-up using Next.js. You will:

1. Convert your markdown into embeddings using OpenAI.
2. Store you embeddings in Postgres using pgvector.
3. Deploy a function for answering your users' questions.

You can read our [Supabase Clippy](/blog/chatgpt-supabase-docs) blog post for a full example.

We assume that you have a Next.js project with a collection of `.mdx` files nested inside your `pages` directory. We will start developing locally with the Supabase CLI and then push our local database changes to our hosted Supabase project. You can find the [full Next.js example on GitHub](https://github.com/supabase-community/nextjs-openai-doc-search).

## Create a project

1. [Create a new project](/dashboard) in the Supabase Dashboard.
1. Enter your project details.
1. Wait for the new database to launch.

## Prepare the database

Let's prepare the database schema. We can use the "OpenAI Vector Search" quickstart in the [SQL Editor](/dashboard/project/_/sql), or you can copy/paste the SQL below and run it yourself.

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="dashboard"
  queryGroup="database-method"
>
<TabPanel id="dashboard" label="Dashboard">

1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.
2. Click **OpenAI Vector Search**.
3. Click **Run**.

</TabPanel>
<TabPanel id="sql" label="SQL">

<StepHikeCompact>

  <StepHikeCompact.Step step={1}>

    <StepHikeCompact.Details title="Set up Supabase locally">

    Make sure you have the latest version of the [Supabase CLI installed](/docs/guides/cli/getting-started).

    Initialize Supabase in the root directory of your app.

    </StepHikeCompact.Details>

    <StepHikeCompact.Details>

    ```bash
    supabase init
    ```

    </StepHikeCompact.Details>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={2}>
    <StepHikeCompact.Details title="Create a migrations file">

    To make changes to our local database, we need to create a new migration. This will create a new `.sql` file in our `supabase/migrations` folder, where we can write SQL that will be applied to our local database when starting Supabase locally.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```bash
      supabase migration new init
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>
    <StepHikeCompact.Details title="Enable the pgvector extension">

    Copy the following SQL line into the newly created migration file to enable the pgvector extension.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```sql
      -- Enable pgvector extension
      create extension if not exists vector with schema public;
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>
    <StepHikeCompact.Details title="Create the database schema">

    Copy these SQL queries to your migration file. It will create two tables in our database schema.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

    ```sql
    -- Stores the checksum of our pages.
    -- This ensures that we only regenerate embeddings
    -- when the page content has changed.
    create table "public"."nods_page" (
      id bigserial primary key,
      parent_page_id bigint references public.nods_page,
      path text not null unique,
      checksum text,
      meta jsonb,
      type text,
      source text
    );
    alter table "public"."nods_page"
      enable row level security;

    -- Stores the actual embeddings with some metadata
    create table "public"."nods_page_section" (
      id bigserial primary key,
      page_id bigint not null references public.nods_page on delete cascade,
      content text,
      token_count int,
      embedding extensions.vector(1536),
      slug text,
      heading text
    );
    alter table "public"."nods_page_section"
      enable row level security;
    ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={4}>
    <StepHikeCompact.Details title="Create similarity search database function">

    Anytime the user sends a query, we want to find the content that's relevant to their questions. We can do this using pgvector's similarity search.

    These are quite complex SQL operations, so let's wrap them in database functions that we can call from our frontend using [RPC](/docs/reference/javascript/rpc).

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

    ```sql
    -- Create embedding similarity search functions
    create or replace function match_page_sections(
        embedding extensions.vector(1536),
        match_threshold float,
        match_count int,
        min_content_length int
    )
    returns table (
        id bigint,
        page_id bigint,
        slug text,
        heading text,
        content text,
        similarity float
    )
    language plpgsql
    as $$
    #variable_conflict use_variable
    begin
      return query
      select
        nods_page_section.id,
        nods_page_section.page_id,
        nods_page_section.slug,
        nods_page_section.heading,
        nods_page_section.content,
        (nods_page_section.embedding <#> embedding) * -1 as similarity
      from nods_page_section

      -- We only care about sections that have a useful amount of content
      where length(nods_page_section.content) >= min_content_length

      -- The dot product is negative because of a Postgres limitation, so we negate it
      and (nods_page_section.embedding <#> embedding) * -1 > match_threshold

      -- OpenAI embeddings are normalized to length 1, so
      -- cosine similarity and dot product will produce the same results.
      -- Using dot product which can be computed slightly faster.
      --
      -- For the different syntaxes, see https://github.com/pgvector/pgvector
      order by nods_page_section.embedding <#> embedding

      limit match_count;
    end;
    $$;
    ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={5}>
    <StepHikeCompact.Details title="Start Supabase Locally">

    Start Supabase locally. At this point all files in `supabase/migrations` will be applied to your database and you're ready to go.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```bash
      supabase start
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={6}>
    <StepHikeCompact.Details title="Push changes to your Supabase database">

    Once ready, you can link your local project to your cloud hosted Supabase project and push the local changes to your hosted instance.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```bash
      supabase link --project-ref=your-project-ref

      supabase db push
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

</StepHikeCompact>

</TabPanel>
</Tabs>

## Pre-process the knowledge base at build time

With our database set up, we need to process and store all `.mdx` files in the `pages` directory. You can find the full script [here](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/lib/generate-embeddings.ts), or follow the steps below:

<StepHikeCompact>

  <StepHikeCompact.Step step={1}>
    <StepHikeCompact.Details title="Generate Embeddings">

    Create a new file `lib/generate-embeddings.ts` and copy the code over from [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/lib/generate-embeddings.ts).

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```bash
      curl \
      https://raw.githubusercontent.com/supabase-community/nextjs-openai-doc-search/main/lib/generate-embeddings.ts \
      -o "lib/generate-embeddings.ts"
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={2}>
    <StepHikeCompact.Details title="Set up environment variables">

    We need some environment variables to run the script. Add them to your `.env` file and make sure your `.env` file is not committed to source control!
    You can get your local Supabase credentials by running `supabase status`.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```bash
      NEXT_PUBLIC_SUPABASE_URL=
      NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
      SUPABASE_SERVICE_ROLE_KEY=

      # Get your key at https://platform.openai.com/account/api-keys
      OPENAI_API_KEY=
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>
    <StepHikeCompact.Details title="Run script at build time">

    Include the script in your `package.json` script commands to enable Vercel to automatically run it at build time.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```json
      "scripts": {
        "dev": "next dev",
        "build": "pnpm run embeddings && next build",
        "start": "next start",
        "embeddings": "tsx lib/generate-embeddings.ts"
      },
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

</StepHikeCompact>

## Create text completion with OpenAI API

Anytime a user asks a question, we need to create an embedding for their question, perform a similarity search, and then send a text completion request to the OpenAI API with the query and then context content merged together into a prompt.

All of this is glued together in a [Vercel Edge Function](https://vercel.com/docs/concepts/functions/edge-functions), the code for which can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/pages/api/vector-search.ts).

<StepHikeCompact>

  <StepHikeCompact.Step step={1}>
    <StepHikeCompact.Details title="Create Embedding for Question">

    In order to perform similarity search we need to turn the question into an embedding.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```ts
      const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${openAiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          model: 'text-embedding-ada-002',
          input: sanitizedQuery.replaceAll('\n', ' '),
        }),
      })

      if (embeddingResponse.status !== 200) {
        throw new ApplicationError('Failed to create embedding for question', embeddingResponse)
      }

      const {
        data: [{ embedding }],
      } = await embeddingResponse.json()
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={2}>
    <StepHikeCompact.Details title="Perform similarity search">

    Using the `embeddingResponse` we can now perform similarity search by performing an remote procedure call (RPC) to the database function we created earlier.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```ts
      const { error: matchError, data: pageSections } = await supabaseClient.rpc(
        'match_page_sections',
        {
          embedding,
          match_threshold: 0.78,
          match_count: 10,
          min_content_length: 50,
        }
      )
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

  <StepHikeCompact.Step step={3}>
    <StepHikeCompact.Details title="Perform text completion request">

    With the relevant content for the user's question identified, we can now build the prompt and make a text completion request via the OpenAI API.

    If successful, the OpenAI API will respond with a `text/event-stream` response that we can forward to the client where we'll process the event stream to smoothly print the answer to the user.

    </StepHikeCompact.Details>

    <StepHikeCompact.Code>

      ```ts
      const prompt = codeBlock`
        ${oneLine`
          You are a very enthusiastic Supabase representative who loves
          to help people! Given the following sections from the Supabase
          documentation, answer the question using only that information,
          outputted in markdown format. If you are unsure and the answer
          is not explicitly written in the documentation, say
          "Sorry, I don't know how to help with that."
        `}

        Context sections:
        ${contextText}

        Question: """
        ${sanitizedQuery}
        """

        Answer as markdown (including related code snippets if available):
      `

      const completionOptions: CreateCompletionRequest = {
        model: 'gpt-3.5-turbo-instruct',
        prompt,
        max_tokens: 512,
        temperature: 0,
        stream: true,
      }

      const response = await fetch('https://api.openai.com/v1/completions', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${openAiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(completionOptions),
      })

      if (!response.ok) {
        const error = await response.json()
        throw new ApplicationError('Failed to generate completion', error)
      }

      // Proxy the streamed SSE response from OpenAI
      return new Response(response.body, {
        headers: {
          'Content-Type': 'text/event-stream',
        },
      })
      ```

    </StepHikeCompact.Code>

  </StepHikeCompact.Step>

</StepHikeCompact>

## Display the answer on the frontend

In a last step, we need to process the event stream from the OpenAI API and print the answer to the user. The full code for this can be found on [GitHub](https://github.com/supabase-community/nextjs-openai-doc-search/blob/main/components/SearchDialog.tsx).

```ts
const handleConfirm = React.useCallback(
  async (query: string) => {
    setAnswer(undefined)
    setQuestion(query)
    setSearch('')
    dispatchPromptData({ index: promptIndex, answer: undefined, query })
    setHasError(false)
    setIsLoading(true)

    const eventSource = new SSE(`api/vector-search`, {
      headers: {
        [REDACTED_SECRET] ?? '',
        Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY}`,
        'Content-Type': 'application/json',
      },
      payload: JSON.stringify({ query }),
    })

    function handleError<T>(err: T) {
      setIsLoading(false)
      setHasError(true)
      console.error(err)
    }

    eventSource.addEventListener('error', handleError)
    eventSource.addEventListener('message', (e: any) => {
      try {
        setIsLoading(false)

        if (e.data === '[DONE]') {
          setPromptIndex((x) => {
            return x + 1
          })
          return
        }

        const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
        const text = completionResponse.choices[0].text

        setAnswer((answer) => {
          const currentAnswer = answer ?? ''

          dispatchPromptData({
            index: promptIndex,
            answer: currentAnswer + text,
          })

          return (answer ?? '') + text
        })
      } catch (err) {
        handleError(err)
      }
    })

    eventSource.stream()

    eventSourceRef.current = eventSource

    setIsLoading(true)
  },
  [promptIndex, promptData]
)
```

## Learn more

Want to learn more about the awesome tech that is powering this?

- Read about how we built [ChatGPT for the Supabase Docs](/blog/chatgpt-supabase-docs).
- Read the pgvector Docs for [Embeddings and vector similarity](/docs/guides/database/extensions/pgvector)
- Watch Greg's video for a full breakdown:

<div class="video-container">
  <iframe
    src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
    frameBorder="1"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowFullScreen
  ></iframe>
</div>


path: references/guides/ai/examples/openai.mdx
content: ---
id: 'examples-openai'
title: 'Generating OpenAI GPT3 completions'
description: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.'
subtitle: 'Generate GPT text completions using OpenAI and Supabase Edge Functions.'
video: 'https://www.youtube-nocookie.com/v/29p8kIqyU_Y'
tocVideo: '29p8kIqyU_Y'
---

OpenAI provides a [completions API](https://platform.openai.com/docs/api-reference/completions) that allows you to use their generative GPT models in your own applications.

OpenAI's API is intended to be used from the server-side. Supabase offers Edge Functions to make it easy to interact with third party APIs like OpenAI.

## Setup Supabase project

If you haven't already, [install the Supabase CLI](/docs/guides/cli) and initialize your project:

```shell
supabase init
```

## Create edge function

Scaffold a new edge function called `openai` by running:

```shell
supabase functions new openai
```

A new edge function will now exist under `./supabase/functions/openai/index.ts`.

We'll design the function to take your user's query (via POST request) and forward it to OpenAI's API.

```ts index.ts
import OpenAI from 'https://deno.land/x/openai@v4.24.0/mod.ts'

Deno.serve(async (req) => {
  const { query } = await req.json()
  const [REDACTED_SECRET]'OPENAI_API_KEY')
  const openai = new OpenAI({
    [REDACTED_SECRET],
  })

  // Documentation here: https://github.com/openai/openai-node
  const chatCompletion = await openai.chat.completions.create({
    messages: [{ role: 'user', content: query }],
    // Choose model from here: https://platform.openai.com/docs/models
    model: 'gpt-3.5-turbo',
    stream: false,
  })

  const reply = chatCompletion.choices[0].message.content

  return new Response(reply, {
    headers: { 'Content-Type': 'text/plain' },
  })
})
```

Note that we are setting `stream` to `false` which will wait until the entire response is complete before returning. If you wish to stream GPT's response word-by-word back to your client, set `stream` to `true`.

## Create OpenAI key

You may have noticed we were passing `OPENAI_API_KEY` in the Authorization header to OpenAI. To generate this key, go to https://platform.openai.com/account/api-keys and create a new secret key.

After getting the key, copy it into a new file called `.env.local` in your `./supabase` folder:

```
OPENAI_API_KEY=your-key-here
```

## Run locally

Serve the edge function locally by running:

```bash
supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt
```

Notice how we are passing in the `.env.local` file.

Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/openai.

```bash
curl -i --location --request POST http://localhost:54321/functions/v1/openai \
  --header 'Content-Type: application/json' \
  --data '{"query":"What is Supabase?"}'
```

You should see a GPT response come back from OpenAI!

## Deploy

Deploy your function to the cloud by running:

```bash
supabase functions deploy --no-verify-jwt openai
supabase secrets set --env-file ./supabase/.env.local
```

## Go deeper

If you're interesting in learning how to use this to build your own ChatGPT, read [the blog post](/blog/chatgpt-supabase-docs) and check out the video:

<div class="video-container">
  <iframe
    src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
    frameBorder="1"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowFullScreen
  ></iframe>
</div>


path: references/guides/ai/examples/semantic-image-search-amazon-titan.mdx
content: ---
id: 'semantic-image-search-python-amazon-titan'
title: 'Semantic Image Search with Amazon Titan'
description: 'Implement semantic image search with Amazon Titan and Supabase Vector in Python.'
subtitle: 'Implement semantic image search with Amazon Titan and Supabase Vector in Python.'
tocVideo: 'A3uND5sgiO0'
---

[Amazon Bedrock](https://aws.amazon.com/bedrock) is a fully managed service that offers a choice of high-performing foundation models (FMs) from leading AI companies like AI21 Labs, Anthropic, Cohere, Meta, Mistral AI, Stability AI, and Amazon. Each model is accessible through a common API which implements a broad set of features to help build generative AI applications with security, privacy, and responsible AI in mind.

[Amazon Titan](https://aws.amazon.com/bedrock/titan/) is a family of foundation models (FMs) for text and image generation, summarization, classification, open-ended Q&A, information extraction, and text or image search.

In this guide we'll look at how we can get started with Amazon Bedrock and Supabase Vector in Python using the Amazon Titan multimodal model and the [vecs client](/docs/guides/ai/vecs-python-client).

You can find the full application code as a Python Poetry project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search).

## Create a new Python project with Poetry

[Poetry](https://python-poetry.org/) provides packaging and dependency management for Python. If you haven't already, install poetry via pip:

```shell
pip install poetry
```

Then initialize a new project:

```shell
poetry new aws_bedrock_image_search
```

## Spin up a Postgres database with pgvector

If you haven't already, head over to [database.new](https://database.new) and create a new project. Every Supabase project comes with a full Postgres database and the [pgvector extension](/docs/guides/database/extensions/pgvector) preconfigured.

When creating your project, make sure to note down your database password as you will need it to construct the `DB_URL` in the next step.

You can find your database connection string on your project dashboard, click [Connect](/dashboard/project/_?showConnect=true). Use the Session pooler connection string which looks like this:

```txt
postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
```

## Install the dependencies

We will need to add the following dependencies to our project:

- [`vecs`](https://github.com/supabase/vecs#vecs): Supabase Vector Python Client.
- [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html): AWS SDK for Python.
- [`matplotlib`](https://matplotlib.org/): for displaying our image result.

```shell
poetry add vecs boto3 matplotlib
```

## Import the necessary dependencies

At the top of your main python script, import the dependencies and store your `DB URL` from above in a variable:

```python
import sys
import boto3
import vecs
import json
import base64
from matplotlib import pyplot as plt
from matplotlib import image as mpimg
from typing import Optional

DB_CONNECTION = "postgresql://postgres.[PROJECT-REF]:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres"
```

Next, get the [credentials to your AWS account](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) and instantiate the `boto3` client:

```python
bedrock_client = boto3.client(
    'bedrock-runtime',
    region_name='us-west-2',
    # Credentials from your AWS account
    aws_access_key_id='<replace_your_own_credentials>',
    aws_secret_access_key='<replace_your_own_credentials>',
    aws_session_token='<replace_your_own_credentials>',
)
```

## Create embeddings for your images

In the root of your project, create a new folder called `images` and add some images. You can use the images from the example project on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/aws_bedrock_image_search/images) or you can find license free images on [Unsplash](https://unsplash.com).

To send images to the Amazon Bedrock API we need to need to encode them as `base64` strings. Create the following helper methods:

```python
def readFileAsBase64(file_path):
    """Encode image as base64 string."""
    try:
        with open(file_path, "rb") as image_file:
            input_image = base64.b64encode(image_file.read()).decode("utf8")
        return input_image
    except:
        print("bad file name")
        sys.exit(0)


def construct_bedrock_image_body(base64_string):
    """Construct the request body.

    https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-titan-embed-mm.html
    """
    return json.dumps(
        {
            "inputImage": base64_string,
            "embeddingConfig": {"outputEmbeddingLength": 1024},
        }
    )


def get_embedding_from_titan_multimodal(body):
    """Invoke the Amazon Titan Model via API request."""
    response = bedrock_client.invoke_model(
        body=body,
        modelId="amazon.titan-embed-image-v1",
        accept="application/json",
        contentType="application/json",
    )

    response_body = json.loads(response.get("body").read())
    print(response_body)
    return response_body["embedding"]


def encode_image(file_path):
    """Generate embedding for the image at file_path."""
    base64_string = readFileAsBase64(file_path)
    body = construct_bedrock_image_body(base64_string)
    emb = get_embedding_from_titan_multimodal(body)
    return emb
```

Next, create a `seed` method, which will create a new Supabase Vector Collection, generate embeddings for your images, and upsert the embeddings into your database:

```python
def seed():
    # create vector store client
    vx = vecs.create_client(DB_CONNECTION)

    # get or create a collection of vectors with 1024 dimensions
    images = vx.get_or_create_collection(name="image_vectors", dimension=1024)

    # Generate image embeddings with Amazon Titan Model
    img_emb1 = encode_image('./images/one.jpg')
    img_emb2 = encode_image('./images/two.jpg')
    img_emb3 = encode_image('./images/three.jpg')
    img_emb4 = encode_image('./images/four.jpg')

    # add records to the *images* collection
    images.upsert(
        records=[
            (
                "one.jpg",       # the vector's identifier
                img_emb1,        # the vector. list or np.array
                {"type": "jpg"}  # associated  metadata
            ), (
                "two.jpg",
                img_emb2,
                {"type": "jpg"}
            ), (
                "three.jpg",
                img_emb3,
                {"type": "jpg"}
            ), (
                "four.jpg",
                img_emb4,
                {"type": "jpg"}
            )
        ]
    )
    print("Inserted images")

    # index the collection for fast search performance
    images.create_index()
    print("Created index")
```

Add this method as a script in your `pyproject.toml` file:

```toml
[tool.poetry.scripts]
seed = "image_search.main:seed"
search = "image_search.main:search"
```

After activating the virtual environment with `poetry shell` you can now run your seed script via `poetry run seed`. You can inspect the generated embeddings in your Supabase Dashboard by visiting the [Table Editor](/dashboard/project/_/editor), selecting the `vecs` schema, and the `image_vectors` table.

## Perform an image search from a text query

We can use Supabase Vector to query our embeddings. We can either use an image as the search input or generate an embedding from a string input:

```python
def search(query_term: Optional[str] = None):
    if query_term is None:
        query_term = sys.argv[1]

    # create vector store client
    vx = vecs.create_client(DB_CONNECTION)
    images = vx.get_or_create_collection(name="image_vectors", dimension=1024)

    # Encode text query
    text_emb = get_embedding_from_titan_multimodal(json.dumps(
        {
            "inputText": query_term,
            "embeddingConfig": {"outputEmbeddingLength": 1024},
        }
    ))

    # query the collection filtering metadata for "type" = "jpg"
    results = images.query(
        data=text_emb,                      # required
        limit=1,                            # number of records to return
        filters={"type": {"$eq": "jpg"}},   # metadata filters
    )
    result = results[0]
    print(result)
    plt.title(result)
    image = mpimg.imread('./images/' + result)
    plt.imshow(image)
    plt.show()
```

By limiting the query to one result, we can show the most relevant image to the user. Finally we use `matplotlib` to show the image result to the user.

Go ahead and test it out by running `poetry run search` and you will be presented with an image of a "bike in front of a red brick wall".

## Conclusion

With just a couple of lines of Python you are able to implement image search as well as reverse image search using the Amazon Titan multimodal model and Supabase Vector.


path: references/guides/ai/going-to-prod.mdx
content: ---
id: 'ai-going-to-prod'
title: 'Going to Production'
description: 'Checklist for going to production with your AI application.'
subtitle: 'Going to production checklist for AI applications.'
sidebar_label: 'Going to Production'
---

This guide will help you to prepare your application for production. We'll provide actionable steps to help you scale your application, ensure that it is reliable, can handle the load, and provide optimal accuracy for your use case.

See our [Engineering for Scale](/docs/guides/ai/engineering-for-scale) guide for more information about engineering at scale.

## Do you need indexes?

Sequential scans will result in significantly higher latencies and lower throughput, guaranteeing 100% accuracy and not being RAM bound.

There are a couple of cases where you might not need indexes:

- You have a small dataset and don't need to scale it.
- You are not expecting high amounts of vector search queries per second.
- You need to guarantee 100% accuracy.

You don't have to create indexes in these cases and can use sequential scans instead. This type of workload will not be RAM bound and will not require any additional resources but will result in higher latencies and lower throughput. Extra CPU cores may help to improve queries per second, but it will not help to improve latency.

On the other hand, if you need to scale your application, you will need to [create indexes](/docs/guides/ai/vector-indexes). This will result in lower latencies and higher throughput, but will require additional RAM to make use of Postgres Caching. Also, using indexes will result in lower accuracy, since you are replacing exact (KNN) search with approximate (ANN) search.

## HNSW vs IVFFlat indexes

`pgvector` supports two types of indexes: HNSW and IVFFlat. We recommend using [HNSW](/docs/guides/ai/vector-indexes/hnsw-indexes) because of its [performance](/blog/increase-performance-pgvector-hnsw#hnsw-performance-1536-dimensions) and [robustness against changing data](/docs/guides/ai/vector-indexes/hnsw-indexes#when-should-you-create-hnsw-indexes).

<Image
  alt="dbpedia embeddings comparing ivfflat and hnsw queries-per-second using the 4XL compute add-on"
  src={{
    light: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--light.png',
    dark: '/docs/img/ai/going-prod/dbpedia-ivfflat-vs-hnsw-4xl--dark.png',
  }}
  zoomable
/>

## HNSW, understanding `ef_construction`, `ef_search`, and `m`

Index build parameters:

- `m` is the number of bi-directional links created for every new element during construction. Higher `m` is suitable for datasets with high dimensionality and/or high accuracy requirements. Reasonable values for `m` are between 2 and 100. Range 12-48 is a good starting point for most use cases (16 is the default value).

- `ef_construction` is the size of the dynamic list for the nearest neighbors (used during the construction algorithm). Higher `ef_construction` will result in better index quality and higher accuracy, but it will also increase the time required to build the index. `ef_construction` has to be at least 2 \* `m` (64 is the default value). At some point, increasing `ef_construction` does not improve the quality of the index. You can measure accuracy when `ef_search`=`ef_construction`: if accuracy is lower than 0.9, then there is room for improvement.

Search parameters:

- `ef_search` is the size of the dynamic list for the nearest neighbors (used during the search). Increasing `ef_search` will result in better accuracy, but it will also increase the time required to execute a query (40 is the default value).

<Image
  alt="dbpedia embeddings comparing hnsw queries-per-second using different build parameters"
  src={{
    light: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--light.png',
    dark: '/docs/img/ai/going-prod/dbpedia-hnsw-build-parameters--dark.png',
  }}
  zoomable
/>

## IVFFlat, understanding `probes` and `lists`

Indexes used for approximate vector similarity search in pgvector divides a dataset into partitions. The number of these partitions is defined by the `lists` constant. The `probes` controls how many lists are going to be searched during a query.

The values of lists and probes directly affect accuracy and queries per second (QPS).

- Higher `lists` means an index will be built slower, but you can achieve better QPS and accuracy.
- Higher `probes` means that select queries will be slower, but you can achieve better accuracy.
- `lists` and `probes` are not independent. Higher `lists` means that you will have to use higher `probes` to achieve the same accuracy.

You can find more examples of how `lists` and `probes` constants affect accuracy and QPS in [pgvector 0.4.0 performance](/blog/pgvector-performance) blogpost.

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/going-prod/lists-count--light.png',
    dark: '/docs/img/ai/going-prod/lists-count--dark.png',
  }}
  zoomable
/>

## Performance tips when using indexes

First, a few generic tips which you can pick and choose from:

1. The Supabase managed platform will automatically optimize Postgres configs for you based on your compute add-on. But if you self-host, consider **adjusting your Postgres config** based on RAM & CPU cores. See [example optimizations](https://gist.github.com/egor-romanov/323e2847851bbd758081511785573c08) for more details.
2. Prefer `inner-product` to `L2` or `Cosine` distances if your vectors are normalized (like `text-embedding-ada-002`). If embeddings are not normalized, `Cosine` distance should give the best results with an index.
3. **Pre-warm your database.** Implement the warm-up technique before transitioning to production or running benchmarks.
   - Use [pg_prewarm](https://www.postgresql.org/docs/current/pgprewarm.html) to load the index into RAM `select pg_prewarm('vecs.docs_vec_idx');`. This will help to avoid cold cache issues.
   - Execute 10,000 to 50,000 "warm-up" queries before each benchmark/prod. This will help to utilize cache and buffers more efficiently.
4. **Establish your workload.** Fine-tune `m` and `ef_construction` or `lists` constants for the pgvector index to accelerate your queries (at the expense of a slower build times). For instance, for benchmarks with 1,000,000 OpenAI embeddings, we set `m` and `ef_construction` to 32 and 80, and it resulted in 35% higher QPS than 24 and 56 values respectively.
5. **Benchmark your own specific workloads.** Doing this during cache warm-up helps gauge the best value for the index build parameters, balancing accuracy with queries per second (QPS).

## Going into production

1. Decide if you are going to use indexes or not. You can skip the rest of this guide if you do not use indexes.
2. Over-provision RAM during preparation. You can scale down in step `5`, but it's better to start with a larger size to get the best results for RAM requirements. (We'd recommend at least 8XL if you're using Supabase.)
3. Upload your data to the database. If you use the [`vecs`](/docs/guides/ai/python/api) library, it will automatically generate an index with default parameters.
4. Run a benchmark using randomly generated queries and observe the results. Again, you can use the `vecs` library with the `ann-benchmarks` tool. Do it with default values for index build parameters, you can later adjust them to get the best results.
5. Monitor the RAM usage, and save it as a note for yourself. You would likely want to use a compute add-on in the future that has the same amount of RAM that was used at the moment (both actual RAM usage and RAM used for cache and buffers).
6. Scale down your compute add-on to the one that would have the same amount of RAM used at the moment.
7. Repeat step 3 to load the data into RAM. You should see QPS increase on subsequent runs, and stop when it no longer increases.
8. Run a benchmark using real queries and observe the results. You can use the `vecs` library for that as well with `ann-benchmarks` tool. Tweak `ef_search` for HNSW or `probes` for IVFFlat until you see that both accuracy and QPS match your requirements.
9. If you want higher QPS you can increase `m` and `ef_construction` for HNSW or `lists` for IVFFlat parameters (consider switching from IVF to HNSW). You have to rebuild the index with a higher `m` and `ef_construction` values and repeat steps 6-7 to find the best combination of `m`, `ef_construction` and `ef_search` constants to achieve the best QPS and accuracy values. Higher `m`, `ef_construction` mean that index will build slower, but you can achieve better QPS and accuracy. Higher `ef_search` mean that select queries will be slower, but you can achieve better accuracy.

## Useful links

Don't forget to check out the general [Production Checklist](/docs/guides/platform/going-into-prod) to ensure your project is secure, performant, and will remain available for your users.

You can look at our [Choosing Compute Add-on](/docs/guides/ai/choosing-compute-addon) guide to get a basic understanding of how much compute you might need for your workload.

Or take a look at our [pgvector 0.5.0 performance](/blog/increase-performance-pgvector-hnsw) and [pgvector 0.4.0 performance](/blog/pgvector-performance) blog posts to see what pgvector is capable of and how the above technique can be used to achieve the best results.

<Image
  alt="multi database"
  src={{
    light: '/docs/img/ai/going-prod/size-to-rps--light.png',
    dark: '/docs/img/ai/going-prod/size-to-rps--dark.png',
  }}
  zoomable
/>


path: references/guides/ai/google-colab.mdx
content: ---
id: 'ai-google-colab'
title: 'Google Colab'
description: 'Use Google Colab to manage your Supabase Vector store.'
subtitle: 'Use Google Colab to manage your Supabase Vector store.'
sidebar_label: 'Google 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>

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](/dashboard/). If you don't already have a database, you can create one here: [database.new](https://database.new).

## Create a new notebook

Start by visiting [colab.research.google.com](https://colab.research.google.com/). There you can create a new notebook.

![Google Colab new notebook](/docs/img/ai/google-colab/colab-new.png)

## Install Vecs

We'll use the Supabase Vector client, [Vecs](/docs/guides/ai/vecs-python-client), to manage our collections.

At the top of the notebook add the notebook paste the following code and hit the "execute" button (`ctrl+enter`):

```py
pip install vecs
```

![Install vecs](/docs/img/ai/google-colab/install-vecs.png)

## Connect to your database

On your project dashboard, click [Connect](/dashboard/project/_?showConnect=true). The connection string should look like `postgres://postgres.xxxx:[REDACTED_SECRET]:6543/postgres`

Create a new code block below the install block (`ctrl+m b`) and add the following code using the Postgres URI you copied above:

```py
import vecs

DB_CONNECTION = "postgres://postgres.xxxx:[REDACTED_SECRET]:6543/postgres"

# create vector store client
vx = vecs.create_client(DB_CONNECTION)
```

Execute the code block (`ctrl+enter`). If no errors were returned then your connection was successful.

## Create a collection

Now we're going to create a new collection and insert some documents.

Create a new code block below the install block (`ctrl+m b`). Add the following code to the code block and execute it (`ctrl+enter`):

```py
collection = vx.get_or_create_collection(name="colab_collection", dimension=3)

collection.upsert(
    vectors=[
        (
         "vec0",           # the vector's identifier
         [0.1, 0.2, 0.3],  # the vector. list or np.array
         {"year": 1973}    # associated  metadata
        ),
        (
         "vec1",
         [0.7, 0.8, 0.9],
         {"year": 2012}
        )
    ]
)
```

This will create a table inside your database within the `vecs` schema, called `colab_collection`. You can view the inserted items in the [Table Editor](/dashboard/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.

![Colab documents](/docs/img/ai/google-colab/colab-documents.png)

## Query your documents

Now we can search for documents based on their similarity. Create a new code block and execute the following code:

```py
collection.query(
    query_vector=[0.4,0.5,0.6],  # required
    limit=5,                     # number of records to return
    filters={},                  # metadata filters
    measure="cosine_distance",   # distance measure to use
    include_value=False,         # should distance measure values be returned?
    include_metadata=False,      # should record metadata be returned?
)
```

You will see that this returns two documents in an array `['vec1', 'vec0']`:

![Colab results](/docs/img/ai/google-colab/colab-results.png)

It also returns a warning:

```
Query does not have a covering index for cosine_distance.
```

You can lean more about creating indexes in the [Vecs documentation](https://supabase.github.io/vecs/api/#create-an-index).

## Resources

- Vecs API: [supabase.github.io/vecs/api](https://supabase.github.io/vecs/api)


path: references/guides/ai/hugging-face.mdx
content: ---
id: 'ai-hugging-face'
title: 'Hugging Face Inference API'
description: 'Learn how to integrate hugging face models with Supabase'
sidebar_label: 'Hugging Face'
---

[Hugging Face](https://huggingface.co) is an open source hub for AI/ML models and tools. With over 100,000 machine learning models available, Hugging Face provides a great way to integrate specialized AI & ML tasks into your application.

There are 3 ways to use Hugging Face models in your application:

1. Use the [Transformers](https://huggingface.co/docs/transformers/index) Python library to perform inference in a Python backend.
1. [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings) directly in Edge Functions using Transformers.js.
1. Use Hugging Face's hosted [Inference API](https://huggingface.co/inference-api) to execute AI tasks remotely on Hugging Face servers. This guide will walk you through this approach.

## AI tasks

Below are some of the types of tasks you can perform with Hugging Face:

### Natural language

- [Summarization](https://huggingface.co/tasks/summarization)
- [Text classification](https://huggingface.co/tasks/text-classification)
- [Text generation](https://huggingface.co/tasks/text-generation)
- [Translation](https://huggingface.co/tasks/translation)
- [Fill in the blank](https://huggingface.co/tasks/fill-mask)

### Computer vision

- [Image to text](https://huggingface.co/tasks/image-to-text)
- [Text to image](https://huggingface.co/tasks/text-to-image)
- [Image classification](https://huggingface.co/tasks/image-classification)
- [Video classification](https://huggingface.co/tasks/video-classification)
- [Object detection](https://huggingface.co/tasks/object-detection)
- [Image segmentation](https://huggingface.co/tasks/image-segmentation)

### Audio

- [Text to speech](https://huggingface.co/tasks/text-to-speech)
- [Speech to text](https://huggingface.co/tasks/automatic-speech-recognition)
- [Audio classification](https://huggingface.co/tasks/audio-classification)

See a [full list of tasks](https://huggingface.co/tasks).

## Access token

First generate a Hugging Face access token for your app:

https://huggingface.co/settings/tokens

Name your token based on the app its being used for and the environment. For example, if you are building an image generation app you might create 2 tokens:

- "Image Generator (Dev)"
- "Image Generator (Prod)"

Since we will be using this token for the inference API, choose the `read` role.

<Admonition type="note">

Though it is possible to use the Hugging Face inference API today without an access token, [you may be rate limited](https://huggingface.co/docs/huggingface.js/inference/README#usage).

To ensure you don't experience any unexpected downtime or errors, we recommend creating an access token.

</Admonition>

## Edge Functions

Edge Functions are server-side TypeScript functions that run on-demand. Since Edge Functions run on a server, you can safely give them access to your Hugging Face access token.

<Admonition type="note">

You will need the `supabase` CLI [installed](/docs/guides/cli) for the following commands to work.

</Admonition>

To create a new Edge Function, navigate to your local project and initialize Supabase if you haven't already:

```shell
supabase init
```

Then create an Edge Function:

```shell
supabase functions new text-to-image
```

Create a file called `.env.local` to store your Hugging Face access token:

```shell
HUGGING_FACE_ACCESS_TOKEN=<your-token-here>
```

Let's modify the Edge Function to import Hugging Face's inference client and perform a `text-to-image` request:

```ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { HfInference } from 'https://esm.sh/@huggingface/inference@2.3.2'

const hf = new HfInference(Deno.env.get('HUGGING_FACE_ACCESS_TOKEN'))

serve(async (req) => {
  const { prompt } = await req.json()

  const image = await hf.textToImage(
    {
      inputs: prompt,
      model: 'stabilityai/stable-diffusion-2',
    },
    {
      use_cache: false,
    }
  )

  return new Response(image)
})
```

1. This function creates a new instance of `HfInference` using the `HUGGING_FACE_ACCESS_TOKEN` environment variable.

1. It expects a POST request that includes a JSON request body. The JSON body should include a parameter called `prompt` that represents the text-to-image prompt that we will pass to Hugging Face's inference API.

1. Next we call `textToImage()`, passing in the user's prompt along with the model that we would like to use for the image generation. Today Hugging Face recommends `stabilityai/stable-diffusion-2`, but you can change this to any other text-to-image model. You can see a list of which models are supported for each task by navigating to their [models page](https://huggingface.co/models?pipeline_tag=text-to-image) and filtering by task.

1. We set `use_cache` to `false` so that repeat queries with the same prompt will produce new images. If the task and model you are using is deterministic (will always produce the same result based on the same input), consider setting `use_cache` to `true` for faster responses.

1. The `image` result returned from the API will be a `Blob`. We can pass the `Blob` directly into a `new Response()` which will automatically set the content type and body of the response from the `image`.

Finally let's serve the Edge Function locally to test it:

```shell
supabase functions serve --env-file .env.local --no-verify-jwt
```

Remember to pass in the `.env.local` file using the `--env-file` parameter so that the Edge Function can access the `HUGGING_FACE_ACCESS_TOKEN`.

<Admonition type="note">

For demo purposes we set `--no-verify-jwt` to make it easy to test the Edge Function without passing in a JWT token. In a real application you will need to pass the JWT as a `Bearer` token in the `Authorization` header.

</Admonition>

At this point, you can make an API request to your Edge Function using your preferred frontend framework (Next.js, React, Expo, etc). We can also test from the terminal using `curl`:

```shell
curl --output result.jpg --location --request POST 'http://localhost:54321/functions/v1/text-to-image' \
  --header 'Content-Type: application/json' \
  --data '{"prompt":"Llama wearing sunglasses"}'
```

In this example, your generated image will save to `result.jpg`:

<img
  src="/docs/img/ai/hugging-face/llama-sunglasses-example.png"
  alt="Llama wearing sunglasses example"
  width="400"
  height="400"
/>

## Next steps

You can now create an Edge Function that invokes a Hugging Face task using your model of choice.

Try running some other [AI tasks](#ai-tasks).

## Resources

- Official [Hugging Face site](https://huggingface.co/).
- Official [Hugging Face JS docs](https://huggingface.co/docs/huggingface.js).
- [Generate image captions](/docs/guides/ai/examples/huggingface-image-captioning) using Hugging Face.


path: references/guides/ai/hybrid-search.mdx
content: ---
id: 'ai-hybrid-search'
title: 'Hybrid search'
description: 'Combine keyword search with semantic search to get both direct and contextual results.'
subtitle: 'Combine keyword search with semantic search.'
sidebar_label: 'Hybrid search'
---

Hybrid search combines [full text search](/docs/guides/ai/keyword-search) (searching by keyword) with [semantic search](/docs/guides/ai/semantic-search) (searching by meaning) to identify results that are both directly and contextually relevant to the user's query.

## Use cases for hybrid search

Sometimes a single search method doesn't quite capture what a user is really looking for. For example, if a user searches for "Italian recipes with tomato sauce" on a cooking app, a keyword search would pull up recipes that specifically mention "Italian," "recipes," and "tomato sauce" in the text. However, it might miss out on dishes that are quintessentially Italian and use tomato sauce but don't explicitly label themselves with these words, or use variations like "pasta sauce" or "marinara." On the other hand, a semantic search might understand the culinary context and find recipes that match the intent, such as a traditional "Spaghetti Marinara," even if they don't match the exact keyword phrase. However, it could also suggest recipes that are contextually related but not what the user is looking for, like a "Mexican salsa" recipe, because it understands the context to be broadly about tomato-based sauces.

Hybrid search combines the strengths of both these methods. It would ensure that recipes explicitly mentioning the keywords are prioritized, thus capturing direct hits that satisfy the keyword criteria. At the same time, it would include recipes identified through semantic understanding as being related in meaning or context, like different Italian dishes that traditionally use tomato sauce but might not have been tagged explicitly with the user's search terms. It identifies results that are both directly and contextually relevant to the user's query while ideally minimizing misses and irrelevant suggestions.

## When to consider hybrid search

The decision to use hybrid search depends on what your users are looking for in your app. For a code repository where developers need to find exact lines of code or error messages, keyword search is likely ideal because it matches specific terms. In a mental health forum where users search for advice or experiences related to their feelings, semantic search may be better because it finds results based on the meaning of a query, not just specific words. For a shopping app where customers might search for specific product names yet also be open to related suggestions, hybrid search combines the best of both worlds - finding exact matches while also uncovering similar products based on the shopping context.

## How to combine search methods

Hybrid search merges keyword search and semantic search, but how does this process work?

First, each search method is executed separately. Keyword search, which involves searching by specific words or phrases present in the content, will yield its own set of results. Similarly, semantic search, which involves understanding the context or meaning behind the search query rather than the specific words used, will generate its own unique results.

Now with these separate result lists available, the next step is to combine them into a single, unified list. This is achieved through a process known as “fusion”. Fusion takes the results from both search methods and merges them together based on a certain ranking or scoring system. This system may prioritize certain results based on factors like their relevance to the search query, their ranking in the individual lists, or other criteria. The result is a final list that integrates the strengths of both keyword and semantic search methods.

## Reciprocal Ranked Fusion (RRF)

One of the most common fusion methods is Reciprocal Ranked Fusion (RRF). The key idea behind RRF is to give more weight to the top-ranked items in each individual result list when building the final combined list.

In RRF, we iterate over each record and assign a score (noting that each record could exist in one or both lists). The score is calculated as 1 divided by that record's rank in each list, summed together between both lists. For example, if a record with an ID of `123` was ranked third in the keyword search and ninth in semantic search, it would receive a score of $$\dfrac{1}{3} + \dfrac{1}{9} = 0.444$$. If the record was found in only one list and not the other, it would receive a score of 0 for the other list. The records are then sorted by this score to create the final list. The items with the highest scores are ranked first, and lowest scores ranked last.

This method ensures that items that are ranked high in multiple lists are given a high rank in the final list. It also ensures that items that are ranked high in only a few lists but low in others are not given a high rank in the final list. Placing the rank in the denominator when calculating score helps penalize the low ranking records.

### Smoothing constant `k`

To prevent extremely high scores for items that are ranked first (since we're dividing by the rank), a `k` constant is often added to the denominator to smooth the score:

$$\dfrac{1}{k+rank}$$

This constant can be any positive number, but is typically small. A constant of 1 would mean that a record ranked first would have a score of $$\dfrac{1}{1+1} = 0.5$$ instead of $$1$$. This adjustment can help balance the influence of items that are ranked very high in individual lists when creating the final combined list.

## Hybrid search in Postgres

Let's implement hybrid search in Postgres using `tsvector` (keyword search) and `pgvector` (semantic search).

First we'll create a `documents` table to store the documents that we will search over. This is just an example - adjust this to match the structure of your application.

```sql
create table documents (
  id bigint primary key generated always as identity,
  content text,
  fts tsvector generated always as (to_tsvector('english', content)) stored,
  embedding extensions.vector(512)
);
```

The table contains 4 columns:

- `id` is an auto-generated unique ID for the record. We'll use this later to match records when performing RRF.
- `content` contains the actual text we will be searching over.
- `fts` is an auto-generated `tsvector` column that is generated using the text in `content`. We will use this for [full text search](/docs/guides/database/full-text-search) (search by keyword).
- `embedding` is a [vector column](/docs/guides/ai/vector-columns) that stores the vector generated from our embedding model. We will use this for [semantic search](/docs/guides/ai/semantic-search) (search by meaning). We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model.

Next we'll create indexes on the `fts` and `embedding` columns so that their individual queries will remain fast at scale:

```sql
-- Create an index for the full-text search
create index on documents using gin(fts);

-- Create an index for the semantic vector search
create index on documents using hnsw (embedding vector_ip_ops);
```

For full text search we use a [generalized inverted (GIN) index](https://www.postgresql.org/docs/current/gin.html) which is designed for handling composite values like those stored in a `tsvector`.

For semantic vector search we use an [HNSW index](/docs/guides/ai/vector-indexes/hnsw-indexes), which is a high performing approximate nearest neighbor (ANN) search algorithm. Note that we are using the `vector_ip_ops` (inner product) operator with this index because we plan on using the inner product (`<#>`) operator later in our query. If you plan to use a different operator like cosine distance (`<=>`), be sure to update the index accordingly. For more information, see [distance operators](/docs/guides/ai/vector-indexes#distance-operators).

Finally we'll create our `hybrid_search` function:

```sql
create or replace function hybrid_search(
  query_text text,
  query_embedding extensions.vector(512),
  match_count int,
  full_text_weight float = 1,
  semantic_weight float = 1,
  rrf_k int = 50
)
returns setof documents
language sql
as $$
with full_text as (
  select
    id,
    -- Note: ts_rank_cd is not indexable but will only rank matches of the where clause
    -- which shouldn't be too big
    row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
  from
    documents
  where
    fts @@ websearch_to_tsquery(query_text)
  order by rank_ix
  limit least(match_count, 30) * 2
),
semantic as (
  select
    id,
    row_number() over (order by embedding <#> query_embedding) as rank_ix
  from
    documents
  order by rank_ix
  limit least(match_count, 30) * 2
)
select
  documents.*
from
  full_text
  full outer join semantic
    on full_text.id = semantic.id
  join documents
    on coalesce(full_text.id, semantic.id) = documents.id
order by
  coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
  coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
  desc
limit
  least(match_count, 30)
$$;
```

Let's break this down:

- **Parameters:** The function accepts quite a few parameters, but the main (required) ones are `query_text`, `query_embedding`, and `match_count`.

  - `query_text` is the user's query text (more on this shortly)
  - `query_embedding` is the vector representation of the user's query produced by the embedding model. We chose 512 dimensions for this example, but adjust this to match the size of the embedding vectors generated from your preferred model. This must match the size of the `embedding` vector on the `documents` table (and use the same model).
  - `match_count` is the number of records returned in the `limit` clause.

  The other parameters are optional, but give more control over the fusion process.

  - `full_text_weight` and `semantic_weight` decide how much weight each search method gets in the final score. These are both 1 by default which means they both equally contribute towards the final rank. A `full_text_weight` of 2 and `semantic_weight` of 1 would give full-text search twice as much weight as semantic search.
  - `rrf_k` is the `k` [smoothing constant](#smoothing-constant-k) added to the reciprocal rank. The default is 50.

- **Return type:** The function returns a set of records from our `documents` table.
- **CTE:** We create two [common table expressions (CTE)](https://www.postgresql.org/docs/current/queries-with.html), one for full-text search and one for semantic search. These perform each query individually prior to joining them.
- **RRF:** The final query combines the results from the two CTEs using [reciprocal rank fusion (RRF)](#reciprocal-ranked-fusion-rrf).

## Running hybrid search

To use this function in SQL, we can run:

```sql
select
  *
from
  hybrid_search(
    'Italian recipes with tomato sauce', -- user query
    '[...]'::extensions.vector(512), -- embedding generated from user query
    10
  );
```

In practice, you will likely be calling this from the [Supabase client](/docs/reference/javascript/introduction) or through a custom backend layer. Here is a quick example of how you might call this from an [Edge Function](/docs/guides/functions) using JavaScript:

```tsx
import { createClient } from 'npm:@supabase/supabase-js@2'
import OpenAI from 'npm:openai'

const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseServiceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const openaiApiKey = Deno.env.get('OPENAI_API_KEY')!

Deno.serve(async (req) => {
  // Grab the user's query from the JSON payload
  const { query } = await req.json()

  // Instantiate OpenAI client
  const openai = new OpenAI({ [REDACTED_SECRET] })

  // Generate a one-time embedding for the user's query
  const embeddingResponse = await openai.embeddings.create({
    model: 'text-embedding-3-large',
    input: query,
    dimensions: 512,
  })

  const [{ embedding }] = embeddingResponse.data

  // Instantiate the Supabase client
  // (replace service role key with user's JWT if using Supabase auth and RLS)
  const supabase = createClient(supabaseUrl, supabaseServiceRoleKey)

  // Call hybrid_search Postgres function via RPC
  const { data: documents } = await supabase.rpc('hybrid_search', {
    query_text: query,
    query_embedding: embedding,
    match_count: 10,
  })

  return new Response(JSON.stringify(documents), {
    headers: { 'Content-Type': 'application/json' },
  })
})
```

This uses OpenAI's `text-embedding-3-large` model to generate embeddings (shortened to 512 dimensions for faster retrieval). Swap in your preferred embedding model (and dimension size) accordingly.

To test this, make a `POST` request to the function's endpoint while passing in a JSON payload containing the user's query. Here is an example `POST` request using cURL:

```tsx
curl -i --location --request POST \
  'http://127.0.0.1:54321/functions/v1/hybrid-search' \
  --header 'Authorization: Bearer <anonymous key>' \
  --header 'Content-Type: application/json' \
  --data '{"query":"Italian recipes with tomato sauce"}'
```

For more information on how to create, test, and deploy edge functions, see [Getting started](/docs/guides/functions/quickstart).

## See also

- [Embedding concepts](/docs/guides/ai/concepts)
- [Vector columns](/docs/guides/ai/vector-columns)
- [Vector indexes](/docs/guides/ai/vector-indexes)
- [Semantic search](/docs/guides/ai/semantic-search)
- [Full text (keyword) search](/docs/guides/database/full-text-search)


path: references/guides/ai/integrations/amazon-bedrock.mdx
content: ---
id: 'ai-amazon-bedrock'
title: 'Amazon Bedrock'
description: 'Learn how to integrate Supabase with Amazon Bedrock, a fully managed service of high-performing foundation models.'
sidebar_label: 'Amazon Bedrock'
tocVideo: 'A3uND5sgiO0'
---

[Amazon Bedrock](https://aws.amazon.com/bedrock) is a fully managed service that offers a choice of high-performing foundation models (FMs) from leading AI companies like AI21 Labs, Anthropic, Cohere, Meta, Mistral AI, Stability AI, and Amazon. Each model is accessible through a common API which implements a broad set of features to help build generative AI applications with security, privacy, and responsible AI in mind.

This guide will walk you through an example using Amazon Bedrock SDK with `vecs`. We will create embeddings using the Amazon Titan Embeddings G1 – Text v1.2 (amazon.titan-embed-text-v1) model, insert these embeddings into a Postgres database using vecs, and then query the collection to find the most similar sentences to a given query sentence.

## Create an environment

First, you need to set up your environment. You will need Python 3.7+ with the `vecs` and `boto3` libraries installed.

You can install the necessary Python libraries using pip:

```sh
pip install vecs boto3
```

You'll also need:

- [Credentials to your AWS account](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html)
- [A Postgres Database with the pgvector extension](hosting.md)

## Create embeddings

Next, we will use Amazon’s Titan Embedding G1 - Text v1.2 model to create embeddings for a set of sentences.

```python
import boto3
import vecs
import json

client = boto3.client(
    'bedrock-runtime',
    region_name='us-east-1',
	# Credentials from your AWS account
    aws_access_key_id='<replace_your_own_credentials>',
    aws_secret_access_key='<replace_your_own_credentials>',
    aws_session_token='<replace_your_own_credentials>',
)

dataset = [
    "The cat sat on the mat.",
    "The quick brown fox jumps over the lazy dog.",
    "Friends, Romans, countrymen, lend me your ears",
    "To be or not to be, that is the question.",
]

embeddings = []

for sentence in dataset:
    # invoke the embeddings model for each sentence
    response = client.invoke_model(
        body= json.dumps({"inputText": sentence}),
        modelId= "amazon.titan-embed-text-v1",
        accept = "application/json",
        contentType = "application/json"
    )
    # collect the embedding from the response
    response_body = json.loads(response["body"].read())
    # add the embedding to the embedding list
    embeddings.append((sentence, response_body.get("embedding"), {}))

```

### Store the embeddings with vecs

Now that we have our embeddings, we can insert them into a Postgres database using vecs.

```python
import vecs

DB_CONNECTION = "postgresql://<user>:<password>@<host>:<port>/<db_name>"

# create vector store client
vx = vecs.Client(DB_CONNECTION)

# create a collection named 'sentences' with 1536 dimensional vectors
# to match the default dimension of the Titan Embeddings G1 - Text model
sentences = vx.get_or_create_collection(name="sentences", dimension=1536)

# upsert the embeddings into the 'sentences' collection
sentences.upsert(records=embeddings)

# create an index for the 'sentences' collection
sentences.create_index()
```

### Querying for most similar sentences

Now, we query the `sentences` collection to find the most similar sentences to a sample query sentence. First need to create an embedding for the query sentence. Next, we query the collection we created earlier to find the most similar sentences.

```python
query_sentence = "A quick animal jumps over a lazy one."

# create vector store client
vx = vecs.Client(DB_CONNECTION)

# create an embedding for the query sentence
response = client.invoke_model(
        body= json.dumps({"inputText": query_sentence}),
        modelId= "amazon.titan-embed-text-v1",
        accept = "application/json",
        contentType = "application/json"
    )

response_body = json.loads(response["body"].read())

query_embedding = response_body.get("embedding")

# query the 'sentences' collection for the most similar sentences
results = sentences.query(
    data=query_embedding,
    limit=3,
    include_value = True
)

# print the results
for result in results:
    print(result)
```

This returns the most similar 3 records and their distance to the query vector.

```
('The quick brown fox jumps over the lazy dog.', 0.27600620558852)
('The cat sat on the mat.', 0.609986272479202)
('To be or not to be, that is the question.', 0.744849503688346)
```

## Resources

- [Amazon Bedrock](https://aws.amazon.com/bedrock)
- [Amazon Titan](https://aws.amazon.com/bedrock/titan)
- [Semantic Image Search with Amazon Titan](/docs/guides/ai/examples/semantic-image-search-amazon-titan)


path: references/guides/ai/integrations/llamaindex.mdx
content: ---
id: 'ai-integration-llamaindex'
title: 'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.'
subtitle: 'Learn how to integrate Supabase with LlamaIndex, a data framework for your LLM applications.'
breadcrumb: 'AI Integrations'
---

This guide will walk you through a basic example using the LlamaIndex [`SupabaseVectorStore`](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb).

<$Partial path="database_setup.mdx" />

## Launching a notebook

Launch our [LlamaIndex](https://github.com/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb) notebook in Colab:

<a
  className="w-64"
  href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.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.

## Fill in your OpenAI credentials

Inside the Notebook, add your `OPENAI_API_KEY` key. Find the cell which contains this code:

```py
import os
os.environ['OPENAI_API_KEY'] = "[your_openai_api_key]"
```

## Connecting to your database

Inside the Notebook, find the cell which specifies the `DB_CONNECTION`. It will contain some code like this:

```python
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. You can find the connection string on your project dashboard by clicking [Connect](/dashboard/project/_?showConnect=true).

<Admonition type='note'>

SQLAlchemy requires the connection string to start with `postgresql://` (instead of `postgres://`). Don't forget to rename this after copying the string from the dashboard.

</Admonition>

<Admonition type='note'>

You must use the "connection pooling" string (domain ending in `*.pooler.supabase.com`) with Google Colab since Colab does not support IPv6.

</Admonition>

## 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](/dashboard/project/_/editor/), by selecting the `vecs` schema from the schema dropdown.

![Colab documents](/docs/img/ai/google-colab/colab-documents.png)

## Resources

- Visit the LlamaIndex + `SupabaseVectorStore` [docs](https://developers.llamaindex.ai/python/examples/vector_stores/supabasevectorindexdemo/)
- Visit the official LlamaIndex [repo](https://github.com/jerryjliu/llama_index/)


path: references/guides/ai/integrations/roboflow.mdx
content: ---
id: 'ai-integration-roboflow'
title: 'Roboflow'
subtitle: 'Learn how to integrate Supabase with Roboflow, a tool for running fine-tuned and foundation vision models.'
breadcrumb: 'AI Integrations'
---

In this guide, we will walk through two examples of using [Roboflow Inference](https://inference.roboflow.com) to run fine-tuned and foundation models. We will run inference and save predictions using an object detection model and [CLIP](https://github.com/openai/CLIP).

<$Partial path="database_setup.mdx" />

## Save computer vision predictions

Once you have a trained vision model, you need to create business logic for your application. In many cases, you want to save inference results to a file.

The steps below show you how to run a vision model locally and save predictions to Supabase.

### Preparation: Set up a model

Before you begin, you will need an object detection model trained on your data.

You can [train a model on Roboflow](https://blog.roboflow.com/getting-started-with-roboflow/), leveraging end-to-end tools from data management and annotation to deployment, or [upload custom model weights](https://docs.roboflow.com/deploy/upload-custom-weights) for deployment.

All models have an infinitely scalable API through which you can query your model, and can be run locally.

For this guide, we will use a demo [rock, paper, scissors](https://universe.roboflow.com/roboflow-58fyf/rock-paper-scissors-sxsw) model.

### Step 1: Install and start Roboflow Inference

You will deploy our model locally using Roboflow Inference, a computer vision inference server.

To install and start Roboflow Inference, first install Docker on your machine.

Then, run:

```
pip install inference inference-cli inference-sdk && inference server start
```

An inference server will be available at `http://localhost:9001`.

### Step 2: Run inference on an image

You can run inference on images and videos. Let's run inference on an image.

Create a new Python file and add the following code:

```python
from inference_sdk import InferenceHTTPClient

image = "example.jpg"
MODEL_ID = "rock-paper-scissors-sxsw/11"

client = InferenceHTTPClient(
    api_url="http://localhost:9001",
    [REDACTED_SECRET]"
)
with client.use_model(MODEL_ID):
    predictions = client.infer(image)

print(predictions)
```

Above, replace:

1. The image URL with the name of the image on which you want to run inference.
2. `ROBOFLOW_API_KEY` with your Roboflow API key. [Learn how to retrieve your Roboflow API key](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
3. `MODEL_ID` with your Roboflow model ID. [Learn how to retrieve your model ID](https://docs.roboflow.com/api-reference/workspace-and-project-ids).

When you run the code above, a list of predictions will be printed to the console:

```
{'time': 0.05402109300121083, 'image': {'width': 640, 'height': 480}, 'predictions': [{'x': 312.5, 'y': 392.0, 'width': 255.0, 'height': 110.0, 'confidence': 0.8620790839195251, 'class': 'Paper', 'class_id': 0}]}
```

### Step 3: Save results in Supabase

To save results in Supabase, add the following code to your script:

```python
import os
from supabase import create_client, Client

url: str = os.environ.get("SUPABASE_URL")
key: str = os.environ.get("SUPABASE_KEY")
supabase: Client = create_client(url, key)

result = supabase.table('predictions') \
    .insert({"filename": image, "predictions": predictions}) \
    .execute()
```

You can then query your predictions using the following code:

```python
result = supabase.table('predictions') \
    .select("predictions") \
    .filter("filename", "eq", image) \
    .execute()

print(result)
```

Here is an example result:

```
data=[{'predictions': {'time': 0.08492901099998562, 'image': {'width': 640, 'height': 480}, 'predictions': [{'x': 312.5, 'y': 392.0, 'width': 255.0, 'height': 110.0, 'confidence': 0.8620790839195251, 'class': 'Paper', 'class_id': 0}]}}, {'predictions': {'time': 0.08818970100037404, 'image': {'width': 640, 'height': 480}, 'predictions': [{'x': 312.5, 'y': 392.0, 'width': 255.0, 'height': 110.0, 'confidence': 0.8620790839195251, 'class': 'Paper', 'class_id': 0}]}}] count=None
```

## Calculate and save CLIP embeddings

You can use the Supabase vector database functionality to store and query CLIP embeddings.

Roboflow Inference provides a HTTP interface through which you can calculate image and text embeddings using CLIP.

### Step 1: Install and start Roboflow Inference

See [Step #1: Install and Start Roboflow Inference](#step-1-install-and-start-roboflow-inference) above to install and start Roboflow Inference.

### Step 2: Run CLIP on an image

Create a new Python file and add the following code:

```python
import cv2
import supervision as sv
import requests
import base64
import os

IMAGE_DIR = "images/train/images/"
API_KEY = ""
SERVER_URL = "http://localhost:9001"

results = []

for i, image in enumerate(os.listdir(IMAGE_DIR)):
    print(f"Processing image {image}")
    infer_clip_payload = {
        "image": {
            "type": "base64",
            "value": base64.b64encode(open(IMAGE_DIR + image, "rb").read()).decode("utf-8"),
        },
    }

    res = requests.post(
        f"{SERVER_URL}/clip/embed_image?[REDACTED_SECRET]",
        json=infer_clip_payload,
    )

    embeddings = res.json()['embeddings']

    results.append({
        "filename": image,
        "embeddings": embeddings
    })
```

This code will calculate CLIP embeddings for each image in the directory and print the results to the console.

Above, replace:

1. `IMAGE_DIR` with the directory containing the images on which you want to run inference.
2. `ROBOFLOW_API_KEY` with your Roboflow API key. [Learn how to retrieve your Roboflow API key](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).

You can also calculate CLIP embeddings in the cloud by setting `SERVER_URL` to `https://infer.roboflow.com`.

### Step 3: Save embeddings in Supabase

You can store your image embeddings in Supabase using the Supabase `vecs` Python package:

First, install `vecs`:

```
pip install vecs
```

Next, add the following code to your script to create an index:

```python

import vecs

DB_CONNECTION = "postgresql://postgres:[password]@[host]:[port]/[database]"

vx = vecs.create_client(DB_CONNECTION)

# create a collection of vectors with 3 dimensions
images = vx.get_or_create_collection(name="image_vectors", dimension=512)

for result in results:
    image = result["filename"]
    embeddings = result["embeddings"][0]

    # insert a vector into the collection
    images.upsert(
        records=[
            (
                image,
                embeddings,
                {} # metadata
            )
        ]
    )

images.create_index()
```

Replace `DB_CONNECTION` with the authentication information for your database. You can retrieve this from the Supabase dashboard in `Project Settings > Database Settings`.

You can then query your embeddings using the following code:

```python
infer_clip_payload = {
    "text": "cat",
}

res = requests.post(
    f"{SERVER_URL}/clip/embed_text?[REDACTED_SECRET]",
    json=infer_clip_payload,
)

embeddings = res.json()['embeddings']

result = images.query(
    data=embeddings[0],
    limit=1
)

print(result[0])
```

## Resources

- [Roboflow Inference documentation](https://inference.roboflow.com)
- [Roboflow Getting Started guide](https://blog.roboflow.com/getting-started-with-roboflow/)
- [How to Build a Semantic Image Search Engine with Supabase and OpenAI CLIP](https://blog.roboflow.com/how-to-use-semantic-search-supabase-openai-clip/)


path: references/guides/ai/keyword-search.mdx
content: ---
id: 'ai-keyword-search'
title: 'Keyword search'
description: 'Learn how to search by words or phrases.'
subtitle: 'Learn how to search by words or phrases.'
sidebar_label: 'Keyword search'
---

Keyword search involves locating documents or records that contain specific words or phrases, primarily based on the exact match between the search terms and the text within the data. It differs from [semantic search](/docs/guides/ai/semantic-search), which interprets the meaning behind the query to provide results that are contextually related, even if the exact words aren't present in the text. Semantic search considers synonyms, intent, and natural language nuances to provide a more nuanced approach to information retrieval.

In Postgres, keyword search is implemented using [full-text search](/docs/guides/database/full-text-search). It supports indexing and text analysis for data retrieval, focusing on records that match the search criteria. Postgres' full-text search extends beyond simple keyword matching to address linguistic nuances, making it effective for applications that require precise text queries.

## When and why to use keyword search

Keyword search is particularly useful in scenarios where precision and specificity matter. It's more effective than semantic search when users are looking for information using exact terminology or specific identifiers. It ensures that results directly contain those terms, reducing the chance of retrieving irrelevant information that might be semantically related but not what the user seeks.

For example in technical or academic research databases, researchers often search for specific studies, compounds, or concepts identified by certain terms or codes. Searching for a specific chemical compound using its exact molecular formula or a unique identifier will yield more focused and relevant results compared to a semantic search, which could return a wide range of documents discussing the compound in different contexts. Keyword search ensures documents that explicitly mention the exact term are found, allowing users to access the precise data they need efficiently.

It's also possible to combine keyword search with semantic search to get the best of both worlds. See [Hybrid search](/docs/guides/ai/hybrid-search) for more details.

## Using full-text search

For an in-depth guide to Postgres' full-text search, including how to store, index, and query records, see [Full text search](/docs/guides/database/full-text-search).

## See also

- [Semantic search](/docs/guides/ai/semantic-search)
- [Hybrid search](/docs/guides/ai/hybrid-search)


path: references/guides/ai/langchain.mdx
content: ---
id: 'ai-lang-chain'
title: 'LangChain'
description: 'Learn how to integrate Supabase with LangChain, a popular framework for composing AI, Vectors, and embeddings'
sidebar_label: 'LangChain'
---

[LangChain](https://langchain.com/) is a popular framework for working with AI, Vectors, and embeddings. LangChain supports using Supabase as a [vector store](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase), using the `pgvector` extension.

## Initializing your database

Prepare you database with the relevant tables:

<Tabs
  scrollable
  size="small"
  type="underlined"
  defaultActiveId="dashboard"
  queryGroup="database-method"
>
<TabPanel id="dashboard" label="Dashboard">

1. Go to the [SQL Editor](/dashboard/project/_/sql) page in the Dashboard.
2. Click **LangChain** in the Quick start section.
3. Click **Run**.

</TabPanel>
<TabPanel id="sql" label="SQL">

```sql
-- Enable the pgvector extension to work with embedding vectors
create extension vector
with
  schema extensions;

-- Create a table to store your documents
create table documents (
  id bigserial primary key,
  content text, -- corresponds to Document.pageContent
  metadata jsonb, -- corresponds to Document.metadata
  embedding extensions.vector(1536) -- 1536 works for OpenAI embeddings, change if needed
);

-- Create a function to search for documents
create function match_documents (
  query_embedding extensions.vector(1536),
  match_count int default null,
  filter jsonb DEFAULT '{}'
) returns table (
  id bigint,
  content text,
  metadata jsonb,
  similarity float
)
language plpgsql
as $$
#variable_conflict use_column
begin
  return query
  select
    id,
    content,
    metadata,
    1 - (documents.embedding <=> query_embedding) as similarity
  from documents
  where metadata @> filter
  order by documents.embedding <=> query_embedding
  limit match_count;
end;
$$;
```

</TabPanel>
</Tabs>

## Usage

You can now search your documents using any Node.js application. This is intended to be run on a secure server route.

```js
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'
import { OpenAIEmbeddings } from '@langchain/openai'
import { createClient } from '@supabase/supabase-js'

const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseKey) throw new Error(`Expected SUPABASE_SERVICE_ROLE_KEY`)

const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)

export const run = async () => {
  const client = createClient(url, supabaseKey)

  const vectorStore = await SupabaseVectorStore.fromTexts(
    ['Hello world', 'Bye bye', "What's this?"],
    [{ id: 2 }, { id: 1 }, { id: 3 }],
    new OpenAIEmbeddings(),
    {
      client,
      tableName: 'documents',
      queryName: 'match_documents',
    }
  )

  const resultOne = await vectorStore.similaritySearch('Hello world', 1)

  console.log(resultOne)
}
```

### Simple metadata filtering

Given the above `match_documents` Postgres function, you can also pass a filter parameter to only return documents with a specific metadata field value. This filter parameter is a JSON object, and the `match_documents` function will use the Postgres JSONB Containment operator `@>` to filter documents by the metadata field values you specify. See details on the [Postgres JSONB Containment operator](https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT) for more information.

```js
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'
import { OpenAIEmbeddings } from '@langchain/openai'
import { createClient } from '@supabase/supabase-js'

// First, follow set-up instructions above

const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)

const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)

export const run = async () => {
  const client = createClient(url, privateKey)

  const vectorStore = await SupabaseVectorStore.fromTexts(
    ['Hello world', 'Hello world', 'Hello world'],
    [{ user_id: 2 }, { user_id: 1 }, { user_id: 3 }],
    new OpenAIEmbeddings(),
    {
      client,
      tableName: 'documents',
      queryName: 'match_documents',
    }
  )

  const result = await vectorStore.similaritySearch('Hello world', 1, {
    user_id: 3,
  })

  console.log(result)
}
```

### Advanced metadata filtering

You can also use query builder-style filtering ([similar to how the Supabase JavaScript library works](/docs/reference/javascript/using-filters)) instead of passing an object. Note that since the filter properties will be in the metadata column, you need to use arrow operators (`->` for integer or `->>` for text) as defined in [PostgREST API documentation](https://postgrest.org/en/stable/references/api/tables_views.html?highlight=operators#json-columns) and specify the data type of the property (e.g. the column should look something like `metadata->some_int_value::int`).

```js
import { SupabaseFilterRPCCall, SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'
import { OpenAIEmbeddings } from '@langchain/openai'
import { createClient } from '@supabase/supabase-js'

// First, follow set-up instructions above

const privateKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!privateKey) throw new Error(`Expected env var SUPABASE_SERVICE_ROLE_KEY`)

const url = process.env.SUPABASE_URL
if (!url) throw new Error(`Expected env var SUPABASE_URL`)

export const run = async () => {
  const client = createClient(url, privateKey)

  const embeddings = new OpenAIEmbeddings()

  const store = new SupabaseVectorStore(embeddings, {
    client,
    tableName: 'documents',
  })

  const docs = [
    {
      pageContent:
        'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to expand upon the notion of quantum fluff, a theoretical concept where subatomic particles coalesce to form transient multidimensional spaces. Yet, this abstraction holds no real-world application or comprehensible meaning, reflecting a cosmic puzzle.',
      metadata: { b: 1, c: 10, stuff: 'right' },
    },
    {
      pageContent:
        'This is a long text, but it actually means something because vector database does not understand Lorem Ipsum. So I would need to proceed by discussing the echo of virtual tweets in the binary corridors of the digital universe. Each tweet, like a pixelated canary, hums in an unseen frequency, a fascinatingly perplexing phenomenon that, while conjuring vivid imagery, lacks any concrete implication or real-world relevance, portraying a paradox of multidimensional spaces in the age of cyber folklore.',
      metadata: { b: 2, c: 9, stuff: 'right' },
    },
    { pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'right' } },
    { pageContent: 'hello', metadata: { b: 1, c: 9, stuff: 'wrong' } },
    { pageContent: 'hi', metadata: { b: 2, c: 8, stuff: 'right' } },
    { pageContent: 'bye', metadata: { b: 3, c: 7, stuff: 'right' } },
    { pageContent: "what's this", metadata: { b: 4, c: 6, stuff: 'right' } },
  ]

  await store.addDocuments(docs)

  const funcFilterA: SupabaseFilterRPCCall = (rpc) =>
    rpc
      .filter('metadata->b::int', 'lt', 3)
      .filter('metadata->c::int', 'gt', 7)
      .textSearch('content', `'multidimensional' & 'spaces'`, {
        config: 'english',
      })

  const resultA = await store.similaritySearch('quantum', 4, funcFilterA)

  const funcFilterB: SupabaseFilterRPCCall = (rpc) =>
    rpc
      .filter('metadata->b::int', 'lt', 3)
      .filter('metadata->c::int', 'gt', 7)
      .filter('metadata->>stuff', 'eq', 'right')

  const resultB = await store.similaritySearch('hello', 2, funcFilterB)

  console.log(resultA, resultB)
}
```

## Hybrid search

LangChain supports the concept of a hybrid search, which combines Similarity Search with Full Text Search. Read the official docs to get started: [Supabase Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).

You can install the LangChain Hybrid Search function though our [database.dev package manager](https://database.dev/langchain/hybrid_search).

## Resources

- Official [LangChain site](https://langchain.com/).
- Official [LangChain docs](https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase).
- Supabase [Hybrid Search](https://js.langchain.com/docs/modules/indexes/retrievers/supabase-hybrid).


path: references/guides/ai/python-clients.mdx
content: ---
id: 'ai-python-clients'
title: 'Choosing a Client'
description: 'Learn how to manage vectors using Python'
sidebar_label: 'Choosing a Client'
---

As described in [Structured & Unstructured Embeddings](/docs/guides/ai/structured-unstructured), AI workloads come in many forms.

For data science or ephemeral workloads, the [Supabase Vecs](https://supabase.github.io/vecs/) client gets you started quickly. All you need is a connection string and vecs handles setting up your database to store and query vectors with associated metadata.

<Admonition type="tip">

Click [**Connect**](/dashboard/project/_/?showConnect=true) at the top of any project page to get your connection string.

Copy the URI from the **Shared pooler** option.

</Admonition>

For production python applications with version controlled migrations, we recommend adding first class vector support to your toolchain by [registering the vector type with your ORM](https://github.com/pgvector/pgvector-python). pgvector provides bindings for the most commonly used SQL drivers/libraries including Django, SQLAlchemy, SQLModel, psycopg, asyncpg and Peewee.


path: references/guides/api/rest/auto-generated-docs.mdx
content: ---
id: 'auto-docs'
title: 'Auto-generated documentation'
description: 'Supabase provides documentation that updates automatically.'
---

Supabase generates documentation in the [Dashboard](/dashboard) which updates as you make database changes.

1. Go to the [API](/dashboard/project/_/api) page in the Dashboard.
2. Select any table under **Tables and Views** in the sidebar.
3. Switch between the JavaScript and the cURL docs using the tabs.

<video width="99%" muted playsInline controls={true}>
  <source
    src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/api/api-docs.mp4"
    type="video/mp4"
  />
</video>


path: references/guides/api/sql-to-rest.mdx
content: ---
id: 'sql-to-rest'
title: 'SQL to REST API Translator'
subtitle: 'Translate SQL queries to HTTP requests and Supabase client code'
description: 'Translate SQL queries to HTTP requests and Supabase client code'
---

Sometimes it's challenging to translate SQL queries to the equivalent [PostgREST](https://postgrest.org/) request or Supabase client code. Use this tool to help with this translation.

<Admonition type="note">

PostgREST supports a subset of SQL, so not all SQL queries will translate.

</Admonition>

<SqlToRest
  defaultValue={`select
  title,
  description
from
  books
where
  description ilike '%cheese%'
order by
  title desc
limit
  5
offset
  10`}
/>


path: references/guides/auth/enterprise-sso.mdx
content: ---
title: 'Enterprise Single Sign-On'
description: 'Learn about Single Sign-On support in Supabase Auth for enterprise applications'
---

Supabase Auth supports building enterprise applications that require Single Sign-On (SSO) authentication [with SAML 2.0](/docs/guides/auth/sso/auth-sso-saml).


path: references/guides/database/extensions/pgmq.mdx
content: ---
id: 'pgmq'
title: 'pgmq: Queues'
description: 'pgmq: Managed queues in Postgres'
---

See the [Supabase Queues docs](/docs/guides/queues).


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)