Merge branch 'master' into feat/input-pattern

This commit is contained in:
Jonathan Summers-Muir committed 2024-03-05 16:08:44 +08:00
commit 22e218d4ff
1342 files changed
+18631 -8031

No files matched your search

+4 -2
View File
@@ -8,5 +8,7 @@ mdx = md
[*]
BasedOnStyles = Vale, Custom
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>)
TokenIgnores = (href=".*?")
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>), \
(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \
(?s) *(<Image.*?/>)
TokenIgnores = (href\s*([=:])\s*['"].*?\1), \
@@ -264,7 +264,7 @@ export const gettingstarted: NavMenuConstant = {
],
},
{
name: 'Web app tutorials',
name: 'Web app demos',
items: [
{
name: 'Next.js',
@@ -492,6 +492,12 @@ export const PhoneLoginsItems = [
isDarkMode: true,
hasLightIcon: true,
},
{
name: 'Textlocal SMS Login (Community Supported)',
icon: '/docs/img/icons/textlocal-icon',
url: '/guides/auth/phone-login/textlocal',
linkDescription: 'Textlocal is a cloud-based SMS platform offering bulk messaging services.',
},
]
export const auth = {
@@ -607,6 +613,10 @@ export const auth = {
name: 'Column Level Security',
url: '/guides/auth/column-level-security',
},
{
name: 'Custom Claims & RBAC',
url: '/guides/auth/custom-claims-and-role-based-access-control-rbac',
},
],
},
{
@@ -1656,6 +1666,24 @@ export const platform: NavMenuConstant = {
{ name: 'SSO with Okta', url: '/guides/platform/sso/okta' },
],
},
{
name: 'Terraform',
url: undefined,
items: [
{
name: 'Terraform Provider',
url: '/guides/platform/terraform',
},
{
name: 'Terraform Tutorial',
url: '/guides/platform/terraform/tutorial',
},
{
name: 'Reference',
url: '/guides/platform/terraform/reference',
},
],
},
{
name: 'Production Readiness',
url: undefined,
@@ -22,7 +22,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
const tsDefinition =
hasTsRef && props.typeSpec ? extractTsDocNode(hasTsRef, props.typeSpec) : null
const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : ''
const parameters = hasTsRef && tsDefinition ? generateParameters(tsDefinition) : item.params
const shortText = hasTsRef && tsDefinition ? tsDefinition.signatures[0].comment.shortText : ''
return (
@@ -51,7 +51,7 @@ const RefFunctionSection: React.FC<IRefFunctionSection> = (props) => {
</ReactMarkdown>
</div>
)}
{/* // parameters */}
{/* parameters */}
{parameters && (
<div className="not-prose mt-12">
<h5 className="mb-3 text-base text-foreground">Parameters</h5>
+12
View File
@@ -20,6 +20,8 @@ The toolkit includes:
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}>
@@ -66,8 +68,12 @@ export const examples = [
},
]
{/* <!-- 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}>
@@ -104,8 +110,12 @@ export const integrations = [
},
]
{/* <!-- vale on --> */}
## Case studies
{/* <!-- vale off --> */}
<div className="grid md:grid-cols-12 gap-4 not-prose">
{[
{
@@ -134,3 +144,5 @@ export const integrations = [
</div>
))}
</div>
{/* <!-- vale on --> */}
@@ -30,6 +30,12 @@ Generate types for your project to produce the `types/supabase.ts` file:
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts
```
or in case of local development:
```bash
npx supabase gen types typescript --local > types/supabase.ts
```
These types are generated from your database schema. Given a table `public.movies`, the generated types will look like:
```sql
@@ -26,9 +26,9 @@ Supabase provides you with the option of adding captcha to your sign-in, sign-up
queryGroup="captcha-method"
>
<TabPanel id="hcaptcha-1" label="HCaptcha">
Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the welcome page, copy the **Sitekey** and **Secret key**.
Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the Welcome page, copy the **Sitekey** and **Secret key**.
If you have already signed up and didn't copy this information from the welcome page, you can get the **Secret key** from the settings page.
If you have already signed up and didn't copy this information from the Welcome page, you can get the **Secret key** from the Settings page.
![site_secret_settings.png](/docs/img/guides/auth-captcha/site_secret_settings.png)
@@ -43,7 +43,7 @@ In the Settings page, look for the **Sitekey** section and copy the key.
</TabPanel>
<TabPanel id="turnstile-1" label="Turnstile">
Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below
Go to the [Cloudflare website](https://dash.cloudflare.com/login) and sign up for an account. On the Welcome page, head to the Turnstile section and add a new site. Create a site and take note of the **Sitekey** and **Secret Key** as shown below
![cloudflare_settings.png](/docs/img/guides/auth-captcha/cloudflare_settings.png)
</TabPanel>
</Tabs>
@@ -397,7 +397,7 @@ A full list of the available variables is below:
| `email_input_placeholder` | Your email address |
| `button_label` | Sign in |
| `loading_button_label` | Signing in ... |
| `link_text` | Already have an account? Sign in |
| `link_text` | Send a magic link email |
| `confirmation_text` | Check your email for the magic link |
</TabPanel>
@@ -7,17 +7,14 @@ description: 'Prebuilt, customizable Flutter widgets for authenticating users.'
Flutter Auth UI is a Flutter package containing pre-built widgets for authenticating users.
It is unstyled and can match your brand and aesthetic.
<image src="https://raw.githubusercontent.com/supabase-community/flutter-auth-ui/main/assets/supabase_auth_ui.png" />
![Flutter Auth UI](https://raw.githubusercontent.com/supabase-community/flutter-auth-ui/main/screenshots/supabase_auth_ui.png)
## Add Flutter Auth UI
Add the latest version of the package [supabase-auth-ui](https://pub.dev/packages/supabase_auth_ui) to pubspec.yaml:
```yaml
dependencies:
flutter:
sdk: flutter
supabase_auth_ui: ^0.1.0+2
```bash
flutter pub add supabase_auth_ui
```
### Initialize the Flutter Auth package
@@ -93,9 +90,9 @@ Use SupaPhoneAuth to create a phone authentication form.
```dart
SupaPhoneAuth(
authAction: SupaAuthAction.signUp,
onSuccess: (AuthResponse response) {},
),
authAction: SupaAuthAction.signUp,
onSuccess: (AuthResponse response) {},
),
```
### Social Auth
@@ -107,13 +104,13 @@ Use SupaSocialsAuth to create list of social login buttons.
```dart
SupaSocialsAuth(
socialProviders: [
SocialProviders.apple,
SocialProviders.google,
OAuthProvider.apple,
OAuthProvider.google,
],
colored: true,
redirectUrl: kIsWeb
? null
: 'io.mydomain.myapp://callback',
? null
: 'io.mydomain.myapp://callback',
onSuccess: (Session response) {},
onError: (error) {},
)
+1 -1
View File
@@ -572,7 +572,7 @@ as $$
is_admin boolean;
begin
-- Check if the user is marked as admin in the profiles table
select is_admin into is_admin from profiles where user_id = event->>'user_id'::uuid;
select is_admin into is_admin from profiles where user_id = (event->>'user_id')::uuid;
-- Proceed only if the user is an admin
if is_admin then
@@ -0,0 +1,278 @@
---
id: 'custom-claims-and-role-based-access-control-rbac'
title: 'Custom Claims & Role-based Access Control (RBAC)'
description: 'Use Auth Hooks to add custom claims for managing role-based access control.'
---
Custom Claims are special attributes attached to a user that you can use to control access to portions of your application. For example:
```json
{
"user_role": "admin",
"plan": "TRIAL",
"user_level": 100,
"group_name": "Super Guild!",
"joined_on": "2022-05-20T14:28:18.217Z",
"group_manager": false,
"items": ["toothpick", "string", "ring"]
}
```
To implement Role-Based Access Control (RBAC) with `custom claims`, use a [Custom Access Token Auth Hook](/guides/auth/auth-hooks#hook-custom-access-token). This hook runs before a token is issued. You can use it to add additional claims to the user's JWT.
This guide uses the [Slack Clone example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone) to demonstrate how to add a `user_role` claim and use it in your [Row Level Security (RLS) policies](/guides/auth/row-level-security).
## Create a table to track user roles and permissions
In this example, you will implement two user roles with specific permissions:
- `moderator`: A moderator can delete all messages but not channels.
- `admin`: An admin can delete all messages and channels.
```sql supabase/migrations/init.sql
-- Custom types
create type public.app_permission as enum ('channels.delete', 'messages.delete');
create type public.app_role as enum ('admin', 'moderator');
-- USER ROLES
create table public.user_roles (
id bigint generated by default as identity primary key,
user_id uuid references public.users on delete cascade not null,
role app_role not null,
unique (user_id, role)
);
comment on table public.user_roles is 'Application roles for each user.';
-- ROLE PERMISSIONS
create table public.role_permissions (
id bigint generated by default as identity primary key,
role app_role not null,
permission app_permission not null,
unique (role, permission)
);
comment on table public.role_permissions is 'Application permissions for each role.';
```
<Admonition type="note">
For the [full schema](https://github.com/supabase/supabase/blob/master/examples/slack-clone/nextjs-slack-clone/README.md), see the example application on [GitHub](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone).
</Admonition>
You can now manage your roles and permissions in SQL. For example, to add the mentioned roles and permissions from above, run:
```sql supabase/seed.sql
insert into public.role_permissions (role, permission)
values
('admin', 'channels.delete'),
('admin', 'messages.delete'),
('moderator', 'messages.delete');
```
## Create Auth Hook to apply user role
The [Custom Access Token Auth Hook](/guides/auth/auth-hooks#hook-custom-access-token) runs before a token is issued. You can use it edit the JWT.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="plpgsql"
queryGroup="language"
>
<TabPanel id="plpgsql" label="PL/pgSQL (best performance)">
```sql supabase/migrations/auth_hook.sql
-- Create the auth hook function
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
immutable
as $$
declare
claims jsonb;
user_role public.app_role;
begin
-- Check if the user is marked as admin in the profiles table
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
claims := event->'claims';
if user_role is not null then
-- Set the claim
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
else
claims := jsonb_set(claims, '{user_role}', 'null');
end if;
-- Update the 'claims' object in the original event
event := jsonb_set(event, '{claims}', claims);
-- Return the modified or original event
return event;
end;
$$;
grant usage on schema public to supabase_auth_admin;
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
revoke execute
on function public.custom_access_token_hook
from authenticated, anon;
grant all
on table public.user_roles
to supabase_auth_admin;
revoke all
on table public.user_roles
from authenticated, anon;
create policy "Allow auth admin to read user roles" ON public.user_roles
as permissive for select
to supabase_auth_admin
using (true)
```
</TabPanel>
<TabPanel id="plv8" label="JavaScript (PLV8)">
```sql supabase/migrations/auth_hook.sql
-- Enable the "plv8" extension
create extension plv8;
-- Create the auth hook function
create or replace function custom_access_token_hook(event jsonb)
returns jsonb
language plv8
as $$
var user_role;
// Fetch the current user's user_role from the public user_roles table.
var result = plv8.execute("select role from public.user_roles where user_id = $1", [event.user_id]);
if (result.length > 0) {
user_role = result[0].role;
} else {
// Assign null
user_role = null;
}
// Check if 'claims' exists in the event object; if not, initialize it
if (!event.claims) {
event.claims = {};
}
// Update the level in the claims
event.claims.user_role = user_role;
return event;
$$;
grant usage on schema public to supabase_auth_admin;
grant execute
on function public.custom_access_token_hook
to supabase_auth_admin;
revoke execute
on function public.custom_access_token_hook
from authenticated, anon;
grant all
on table public.user_roles
to supabase_auth_admin;
revoke all
on table public.user_roles
from authenticated, anon;
create policy "Allow auth admin to read user roles" ON public.user_roles
as permissive for select
to supabase_auth_admin
using (true)
```
</TabPanel>
</Tabs>
### Enable the hook
In the dashboard, navigate to [`Authentication > Hooks (Beta)`](/dashboard/project/_/auth/hooks) and select the appropriate PostgreSQL function from the dropdown menu.
When developing locally, follow the [local development](/docs/guides/auth/auth-hooks#local-development) instructions.
<Admonition type="note">
To learn more about Auth Hooks, see the [Auth Hooks docs](/guides/auth/auth-hooks).
</Admonition>
## Accessing custom claims in RLS policies
To utilize Role-Based Access Control (RBAC) in Row Level Security (RLS) policies, create an `authorize` method that reads the user's role from their JWT and checks the role's permissions:
```sql supabase/migrations/init.sql
create function public.authorize(
requested_permission app_permission,
user_id uuid
)
returns boolean as $$
declare
bind_permissions int;
begin
select count(*)
from public.role_permissions
where role_permissions.permission = authorize.requested_permission
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
into bind_permissions;
return bind_permissions > 0;
end;
$$ language plpgsql security definer set search_path = public;
```
<Admonition type="note">
You can read more about using functions in RLS policies in the [RLS guide](/guides/auth/row-level-security#using-functions).
</Admonition>
You can then use the `authorize` method within your RLS policies. For example, to enable the desired delete access, you would add the following policies:
```sql
create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete', auth.uid()) );
create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete', auth.uid()) );
```
## Accessing custom claims in your application
The auth hook will only modify the access token JWT but not the auth response. Therefore, to access the custom claims in your application, e.g. your browser client, or server-side middleware, you will need to decode the `access_token` JWT on the auth session.
In a JavaScript client application you can for example use the [`jwt-decode` package](https://www.npmjs.com/package/jwt-decode):
```js
import { jwtDecode } from 'jwt-decode'
const { subscription: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
if (session) {
const jwt = jwtDecode(session.access_token)
const userRole = jwt.user_role
}
})
```
For server-side logic you can use packages like [express-jwt](https://github.com/auth0/express-jwt), [koa-jwt](https://github.com/stiang/koa-jwt), [PyJWT](https://github.com/jpadilla/pyjwt), [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken), [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer), etc.
## Conclusion
You now have a robust system in place to manage user roles and permissions within your database that automatically propagates to Supabase Auth.
## More resources
- [Auth Hooks](/guides/auth/auth-hooks)
- [Row Level Security](/guides/auth/row-level-security)
- [RLS Functions](/guides/auth/row-level-security#using-functions)
- [Next.js Slack Clone Example](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone)
@@ -108,7 +108,7 @@ curl -X POST 'https://cvwawazfelidkloqmbma.supabase.co/auth/v1/signup' \
The user receives an SMS with a 6-digit pin that you must [verify](/docs/guides/auth/phone-login#verify-a-user) within 60 seconds.
After their phone number is verified, the user can use their phone number and password to sign in with needing to reverify each time:
Once a user's phone number is verified, they can sign in using their phone number and password without requiring phone number re-verification at each sign-in:
<Tabs
scrollable
@@ -0,0 +1,63 @@
---
id: 'auth-textlocal'
title: 'Phone Auth with Textlocal'
subtitle: 'How to set up and use Mobile OTP with Textlocal and Supabase.'
description: 'How to set up and use Mobile OTP with Textlocal and Supabase.'
---
<Admonition type="tip">
To manage costs related to SMS authentication, consider setting project rate limits and configuring CAPTCHA. For additional cost-control strategies, visit the Production Checklist.
Also check country-specific regulations around sending SMS messages, for example, India's TRAI DLT
regulations.
</Admonition>
## Prerequisites
Before you begin, ensure you have the following:
- **A Textlocal account**: Sign up for [TextLocal](https://www.textlocal.com/signup) to start sending SMS messages.
- **A Supabase project**: Necessary for SMS authentication integration. Create your project in the [Supabase Dashboard](https://supabase.com/dashboard).
- **A mobile phone**: To receive SMS messages and test your setup.
## Setting up Textlocal as your SMS provider
To integrate Textlocal with Supabase:
1. [Get a Textlocal API key](#get-a-textlocal-api-key)
2. [Customize your sender name](#customize-your-sender-name-optional)
3. [Configure your Supabase project](#configure-supabase)
### Get a Textlocal API key
1. Log into your Textlocal account and go to `Settings` > `API Keys`.
2. Generate a new API Key. Save your new API key in a safe location.
### Customize your sender name (Optional)
Textlocal defaults to `TXTLCL` as the sender name for all messages. You can customize this to better reflect your brand:
1. In your Textlocal dashboard, go to `Settings` > `All Settings` > `Sender Names`.
2. Change your sender name.
### Configure Supabase
To set up Textlocal as your SMS provider in Supabase, follow these steps:
1. In your Supabase Dashboard, go to the [Auth Providers section](https://supabase.com/dashboard/project/_/auth/providers).
2. From the list of available authentication providers, select `Phone`.
3. Toggle `Enable Phone Provider`.
4. Under `SMS Provider`, select `Textlocal.`
5. Enter your Textlocal API Key and Sender Name.
6. Customize your SMS Message (optional).
## Next steps
To implement Phone Login, see the docs on [using Phone Login](/docs/guides/auth/phone-login#using-phone-login).
## Resources
- [Textlocal Signup](https://www.textlocal.com/sign-up)
- [Supabase Dashboard](https://supabase.com/dashboard)
@@ -203,7 +203,9 @@ It's safe to trust `getUser()` because it sends a request to the Supabase Auth s
<CH.Code className="min-h-[800px]">
```ts middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
import { type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
return await updateSession(request)
@@ -400,7 +402,7 @@ If you have email confirmation turned on (the default), a new user will receive
Change the email template to support a server-side authentication flow.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In each email template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
Go to the [Auth templates](https://supabase.com/dashboard/project/_/auth/templates) page in your dashboard. In the `Confirm signup` template, change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup`.
</StepHikeCompact.Details>
@@ -232,21 +232,20 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
<TabPanel id="flutter" label="Flutter">
## Using native sign-in with Google on Flutter
## Google sign-in for iOS and Android
Native Google sign in with Supabase is done through the [google_sign_in](https://pub.dev/packages/google_sign_in) package for iOS and Android. Follow the instructions on the [google_sign_in](https://pub.dev/packages/google_sign_in) package for plugin specific settings.
Google sign-in with Supabase is done through the [google_sign_in](https://pub.dev/packages/google_sign_in) package for iOS and Android.
When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth.
Follow the code sample below to implement native Google sign in with Supabase in your Flutter app.
Follow the code sample below to implement native Google sign-in with Supabase in your Flutter iOS and Android app.
```dart
import 'package:google_sign_in/google_sign_in.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
...
Future<AuthResponse> _googleSignIn() async {
Future<void> _nativeGoogleSignIn() async {
/// TODO: update the Web client ID with your own.
///
/// Web Client ID that you registered with Google Cloud.
@@ -257,9 +256,6 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
/// iOS Client ID that you registered with Google Cloud.
const iosClientId = 'my-ios.apps.googleusercontent.com';
// Google sign in on Android will work without providing the Android
// Client ID registered on Google Cloud.
final GoogleSignIn googleSignIn = GoogleSignIn(
clientId: iosClientId,
serverClientId: webClientId,
@@ -276,7 +272,7 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
throw 'No ID Token found.';
}
return supabase.auth.signInWithIdToken(
await supabase.auth.signInWithIdToken(
provider: OAuthProvider.google,
idToken: idToken,
accessToken: accessToken,
@@ -287,15 +283,63 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
### Configuration [#flutter-configuration-native-app]
1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Android_ or _iOS_ depending on the mobile operating system your app is built for. Also create a web client ID.
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. You also need to create a web client ID to perform Google sign in on Android.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore.
1. Configure Web, Android, and iOS OAuth credentials. Follow the Platform integration instructions on the [README of google_sign_in package](https://pub.dev/packages/google_sign_in#platform-integration) for Android and iOS.
- For both Android and iOS, you will need to create a Web client ID in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials).
- For Android, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app.
- For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore.
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
3. Finally, add the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. You do not need to add the Android or iOS client ID here. If you need iOS support, enable the `Skip nonce check` option.
3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. If you need iOS support, enable the `Skip nonce check` option.
4. For iOS, add the `CFBundleURLTypes` attributes below into the `/ios/Runner/Info.plist` file.
```xml
<!-- Put me in the [my_project]/ios/Runner/Info.plist file -->
<!-- Google Sign-in Section -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.861823949799-vc35cprkp249096uujjn0vvnmcvjppkn</string>
</array>
</dict>
</array>
<!-- End of the Google Sign-in Section -->
```
Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in.
## Google sign-in for Web, macOS, Windows, and Linux
Google sign-in with Supabase on Web, macOS, Windows, and Linux is done through the [signInWithOAuth](docs/reference/dart/auth-signinwithoauth) method.
This method of signing in is web based, and will open a browser window to perform the sign in. For non-web platforms, the user is brought back to the app via [deep linking](/docs/guides/auth/native-mobile-deep-linking?platform=flutter).
```dart
supabase.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo: 'my-scheme://login-callback',
);
```
This call takes the user to Google's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Supabase Auth before it redirects back to your Flutter application with an access and refresh token representing the user's session.
### Configuration [#flutter-configuration-web]
To use the OAuth 2.0 flow, you will require the following information:
1. Obtain OAuth credentials for your Google Cloud project in the [Credentials](https://console.developers.google.com/apis/credentials) page of the console. If you already have a web client ID, no need to create a new one. When creating a new credential, choose _Web application_. In _Authorized redirect URIs_ enter `https://<project-id>.supabase.co/auth/v1/callback`. This URL will be seen by your users, and you can customize it by configuring [custom domains](/docs/guides/platform/custom-domains).
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. Within _Authorized domains_ make sure you add your Supabase project's domain `<project-id>.supabase.co`. Configure the non-sensitive scopes by making sure the following ones are selected: `.../auth/userinfo.email`, `.../auth/userinfo.profile`, `openid`. If you're selecting other sensitive scopes, your app may require additional verification. In those cases, it's best to use [custom domains](/docs/guides/platform/custom-domains).
3. Finally, add the client ID and secret from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers).
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/utMg6fVmX0U"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
</TabPanel>
@@ -456,6 +500,17 @@ Before you can use Sign in with Google, you need to obtain a [Google Cloud Platf
2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service.
3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_.
Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/P_jZMDmodG4"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
</TabPanel>
<TabPanel id="chrome-extensions" label="Chrome Extensions">
@@ -36,7 +36,7 @@ drop
extension pgtap;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension call `drop extension`.
</TabPanel>
@@ -48,7 +48,7 @@ Most extensions are installed under the `extensions` schema, which is accessible
If you need to restrict user access to tables managed by extensions, we recommend creating a separate schema for installing that specific extension.
Some extensions can only be created under a specific schema, for eg. `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
Some extensions can only be created under a specific schema, for example, `postgis_tiger_geocoder` extension creates a schema named `tiger`. Before enabling such extensions, make sure you have not created a conflicting schema with the same name.
In addition to the pre-configured extensions, you can also install your own SQL extensions directly in the database using Supabase's SQL editor. The SQL code for the extensions, including plpgsql extensions, can be added through the SQL editor.
@@ -21,7 +21,7 @@ The motivation for HypoPG is to allow users to quickly search for an index to im
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "hypopg" and enable the extension.
3. Search for `hypopg` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -35,7 +35,7 @@ create extension hypopg with schema extensions;
drop extension if exists hypopg;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -104,10 +104,10 @@ create index on account(id);
- [`hypo_create_index(text)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#create-a-hypothetical-index): A function to create a hypothetical index.
- [`hypopg_list_indexes`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A View that lists all hypothetical indexes that have been created.
- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as pg_index.
- [`hypopg()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function that lists all hypothetical indexes that have been created with the same format as `pg_index`.
- [`hypopg_get_index_def(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to display the `create index` statement that would create the index.
- [`hypopg_get_relation_size(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to estimate how large a hypothetical index would be.
- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by oid.
- [`hypopg_drop_index(oid)`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove a given hypothetical index by `oid`.
- [`hypopg_reset()`](https://hypopg.readthedocs.io/en/rel1_stable/usage.html#manipulate-hypothetical-indexes): A function to remove all hypothetical indexes.
## Resources
@@ -19,7 +19,7 @@ description: 'Validate json/jsonb with JSON Schema in PostgreSQL.'
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pg_jsonschema" and enable the extension.
3. Search for `pg_jsonschema` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -33,7 +33,7 @@ create extension pg_jsonschema with schema extensions;
drop extension if exists pg_jsonschema;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -4,7 +4,7 @@ title: 'pg_plan_filter: Restrict Total Cost'
description: 'Block queries over a total cost limit'
---
[pg_plan_filter](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
[`pg_plan_filter`](https://github.com/pgexperts/pg_plan_filter) is PostgreSQL extension to block execution of statements where query planner's estimate of the total cost exceeds a threshold. This is intended to give database administrators a way to restrict the contribution an individual query has on database load.
## Enable the extension
@@ -27,7 +27,7 @@ alter database some_db set session_preload_libraries = 'plan_filter';
`plan_filter.statement_cost_limit`: restricts the maximum total cost for executed statements
`plan_filter.limit_select_only`: restricts to `select` statements
Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data e.g. through a function call.
Note that `limit_select_only = true` is not the same as read-only because `select` statements may modify data, for example, through a function call.
## Example
@@ -88,4 +88,4 @@ HINT: The plan for your query shows that it would probably have an excessive ru
## Resources
- Official [pg_plan_filter documentation](https://github.com/pgexperts/pg_plan_filter)
- Official [`pg_plan_filter` documentation](https://github.com/pgexperts/pg_plan_filter)
@@ -21,7 +21,7 @@ PGAudit grants fine grain control over which statements and objects are emitted
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgaudit" and enable the extension.
3. Search for `pgaudit` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -35,7 +35,7 @@ create extension pgaudit;
drop extension if exists pgaudit;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
</TabPanel>
@@ -45,13 +45,13 @@ To disable an extension you can call `drop extension`.
The `pgaudit.log` setting controls which statements to log. Available values include:
- **read**: `SELECT` and `COPY` when the source is a relation or a query.
- **write**: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
- **function**: Function calls and `DO` blocks.
- **role**: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
- **ddl**: All `DDL` that is not included in the `ROLE` class.
- **misc**: Miscellaneous commands, e.g. `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
- **misc_set**: Miscellaneous `SET` commands, e.g. `SET ROLE`.
- `read`: `SELECT` and `COPY` when the source is a relation or a query.
- `write`: `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, and `COPY` when the destination is a relation.
- `function`: Function calls and `DO` blocks.
- `role`: Statements related to roles and privileges: `GRANT`, `REVOKE`, `CREATE/ALTER/DROP ROLE`.
- `ddl`: All `DDL` that is not included in the `ROLE` class.
- `misc`: Miscellaneous commands, for example, `DISCARD`, `FETCH`, `CHECKPOINT`, `VACUUM`, `SET`.
- `misc_set`: Miscellaneous `SET` commands, for example, `SET ROLE`.
- **all**: Include all of the above.
@@ -59,7 +59,7 @@ For a full list of available settings see [settings docs](https://github.com/pga
<Admonition type="note">
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [pgsodium](/docs/guides/database/extensions/pgsodium) or [Vault](/docs/guides/database/vault).
We don't currently support configuring `pgaudit.log_parameter` because it may log secrets in encrypted columns if you are using [`pgsodium`](/docs/guides/database/extensions/pgsodium) or [Vault](/docs/guides/database/vault).
You can upvote this [feature request](https://github.com/orgs/supabase/discussions/20183) if you'd like this restriction lifted.
@@ -67,7 +67,7 @@ You can upvote this [feature request](https://github.com/orgs/supabase/discussio
## Example
Given a pgaudit setting
Given a `pgaudit` setting
{/* prettier-ignore */}
```sql
@@ -4,7 +4,7 @@ title: 'pgjwt: JSON Web Tokens'
description: 'Encode and decode JWTs in PostgreSQL'
---
The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
The [`pgjwt`](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extension allows you to create and parse [JSON Web Tokens (JWTs)](https://en.wikipedia.org/wiki/JSON_Web_Token) within a PostgreSQL database. JWTs are commonly used for authentication and authorization in web applications and services.
## Enable the extension
@@ -19,7 +19,7 @@ The [pgjwt](https://github.com/michelp/pgjwt) (PostgreSQL JSON Web Token) extens
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgjwt" and enable the extension.
3. Search for `pgjwt` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -33,7 +33,7 @@ create extension pgjwt schema extensions;
drop extension if exists pgjwt;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -67,7 +67,7 @@ select
);
```
The pgjwt_encode function returns a string that represents the JWT, which can then be safely transmitted between parties.
The `pgjwt_encode` function returns a string that represents the JWT, which can then be safely transmitted between parties.
{/* prettier-ignore */}
```
@@ -20,7 +20,7 @@ tocVideo: 'Mmmv9g_MiBA'
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgroonga" and enable the extension.
3. Search for `pgroonga` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -34,7 +34,7 @@ create extension pgroonga with schema extensions;
drop extension if exists pgroonga;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
</TabPanel>
@@ -92,7 +92,7 @@ Index Scan using ix_memos_content on memos (cost=0.00..1.11 rows=1 width=36)
(2 rows)
```
The pgroonga index is used to retrieve the result set:
The `pgroonga` index is used to retrieve the result set:
```markdown
| id | content |
@@ -102,7 +102,7 @@ The pgroonga index is used to retrieve the result set:
## Full text search
The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, pgroonga can search any text that contains the keyword case insensitive.
The `&@~` operator performs full text search. It returns any matching results. Unlike `LIKE` operator, `pgroonga` can search any text that contains the keyword case insensitive.
Take the following example:
@@ -4,7 +4,7 @@ title: 'pgrouting: Geospatial Routing'
description: 'Extends PostGIS with Geospatial Routing'
---
[pgRouting](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
[`pgRouting`](http://pgrouting.org) is PostgreSQL and [PostGIS](http://postgis.net) extension adding geospatial routing functionality.
The core functionality of `pgRouting` is a set of path finding algorithms including:
@@ -33,7 +33,7 @@ The core functionality of `pgRouting` is a set of path finding algorithms includ
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgrouting" and enable the extension.
3. Search for `pgrouting` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -47,7 +47,7 @@ create extension pgrouting cascade;
drop extension if exists pgRouting;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
</TabPanel>
@@ -55,9 +55,9 @@ To disable an extension you can call `drop extension`.
## Example
As an example, we'll solve the [traveling salesman problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the pgRouting's `pgr_TSPeuclidean` function from some PostGIS coordinates.
As an example, we'll solve the [traveling salesperson problem](https://en.wikipedia.org/wiki/Travelling_salesman_problem) using the `pgRouting`'s `pgr_TSPeuclidean` function from some PostGIS coordinates.
A summary of the traveling salesman problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
A summary of the traveling salesperson problem is, given a set of city coordinates, solve for a path that goes through each city and minimizes the total distance traveled.
First we populate a table with some X, Y coordinates
@@ -4,13 +4,13 @@ title: 'pgsodium: Encryption Features'
description: 'Encryption library for PostgreSQL'
---
[pgsodium](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [libsodium's](https://doc.libsodium.org/) high-level cryptographic algorithms.
[`pgsodium`](https://github.com/michelp/pgsodium) is a PostgreSQL extension which provides SQL access to [`libsodium`'s](https://doc.libsodium.org/) high-level cryptographic algorithms.
libsodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
`libsodium` is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing, and more. It is a portable, cross-compilable, installable, and packageable fork of the [NaCl](http://nacl.cr.yp.to/) library, with a compatible but extended API to improve usability even further.
The design choices emphasize security and ease of use. But despite the emphasis on high security, primitives are faster across-the-board than most implementations.
pgsodium exposes the following libsodium APIs to SQL:
`pgsodium` exposes the following `libsodium` APIs to SQL:
- [Generating Random Data](https://github.com/michelp/pgsodium/#generating-random-data)
- [Secret key cryptography](https://github.com/michelp/pgsodium/#secret-key-cryptography)
@@ -53,7 +53,7 @@ Note that Supabase projects are already encrypted at rest by default.
## Get the root encryption key for your Supabase project
Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the pgsodium root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
Encryption requires keys. Keeping the keys in the same database as the encrypted data would be unsafe. For more information about managing the `pgsodium` root encryption key on your Supabase project see **[encryption key location](/docs/guides/database/vault#encryption-key-location)**. This key is required to decrypt values stored in [Supabase Vault](/docs/guides/database/vault) and data encrypted with Transparent Column Encryption.
## Enable the extension
@@ -68,7 +68,7 @@ Encryption requires keys. Keeping the keys in the same database as the encrypted
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgsodium" and enable the extension.
3. Search for `pgsodium` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -26,7 +26,7 @@ Let's cover some basic concepts:
1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard.
2. Click on **Extensions** in the sidebar.
3. Search for "pgtap" and enable the extension.
3. Search for `pgtap` and enable the extension.
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -39,7 +39,7 @@ create extension pgtap with schema extensions;
drop extension if exists pgtap;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension you can call `drop extension`.
It's good practice to create the extension within a separate schema (like `extensions`) to keep the `public` schema clean.
@@ -149,7 +149,7 @@ rollback;
API:
- [`function_returns()`](https://pgtap.org/documentation.html#function_returns): Tests that a particular function returns a particular data type
- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (i.e., a “setuid” function).
- [`is_definer()`](https://pgtap.org/documentation.html#is_definer): Tests that a function is a security definer (that is, a `setuid` function).
## Resources
@@ -8,7 +8,7 @@ The `plv8` extension allows you use JavaScript within Postgres.
## Overview
While Postgres natively runs SQL, it can also run other "procedural languages".
While Postgres natively runs SQL, it can also run other procedural languages.
`plv8` allows you to run JavaScript code - specifically any code that runs on the [V8 JavaScript engine](https://v8.dev).
It can be used for database functions, triggers, queries and more.
@@ -39,7 +39,7 @@ create extension plv8;
drop extension if exists plv8;
```
Even though the SQL code is `create extension`, this is the equivalent of "enabling the extension".
Even though the SQL code is `create extension`, this is the equivalent of enabling the extension.
To disable an extension, call `drop extension`.
Procedural languages are automatically installed within `pg_catalog`, so you don't need to specify a schema.
@@ -30,6 +30,8 @@ For this guide we'll use the following example data:
>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description |
| --- | ----------------------------------- | ---------------------- | ------------------------------------------------------------------ |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
@@ -38,6 +40,8 @@ For this guide we'll use the following example data:
| 4 | Green Eggs and Ham | Dr. Seuss | Sam has changing food preferences and eats unusually colored food. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
{/* <!-- vale on --> */}
</TabPanel>
<TabPanel id="sql" label="SQL">
@@ -80,7 +84,7 @@ The functions we'll cover in this guide are:
### `to_tsvector()` [#to-tsvector]
Converts your data into searchable "tokens". `to_tsvector()` stands for "to text search vector". For example:
Converts your data into searchable tokens. `to_tsvector()` stands for "to text search vector." For example:
```sql
select to_tsvector('green eggs and ham');
@@ -91,10 +95,10 @@ Collectively these tokens are called a "document" which Postgres can use for com
### `to_tsquery()` [#to-tsquery]
Converts a query string into "tokens" to match. `to_tsquery()` stands for "to text search query".
Converts a query string into tokens to match. `to_tsquery()` stands for "to text search query."
This conversion step is important because we will want to "fuzzy match" on keywords.
For example if a user searches for "eggs", and a column has the value "egg", we probably still want to return a match.
For example if a user searches for `eggs`, and a column has the value `egg`, we probably still want to return a match.
### Match: `@@` [#match]
@@ -253,11 +257,15 @@ val data = supabase.from("books").select {
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description |
| --- | ----------------------------------- | ----------------- | ----------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
| 5 | Harry Potter and the Goblet of Fire | J.K. Rowling | Fourth year of school starts, big drama ensues. |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -338,11 +346,15 @@ val data = supabase.from("books").select {
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -403,10 +415,14 @@ val data = supabase.from("books").select {
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description |
| --- | ------ | ----------------- | -------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -467,11 +483,15 @@ val data = supabase.from("books").select {
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description |
| --- | --------------------- | ---------------------- | ------------------------------------------- |
| 1 | The Poky Little Puppy | Janette Sebring Lowrey | Puppy is slower than other, bigger animals. |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -514,6 +534,8 @@ from books;
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | fts |
| --- | --------------------------------------------------------------------------------------------------------------- |
| 1 | 'anim':7 'bigger':6 'littl':10 'poki':9 'puppi':1,11 'slower':3 |
@@ -522,6 +544,8 @@ from books;
| 4 | 'chang':3 'color':9 'eat':7 'egg':12 'food':4,10 'green':11 'ham':14 'prefer':5 'sam':1 'unus':8 |
| 5 | 'big':6 'drama':7 'ensu':8 'fire':15 'fourth':1 'goblet':13 'harri':9 'potter':10 'school':4 'start':5 'year':2 |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -578,10 +602,14 @@ val data = supabase.from("books").select {
</TabPanel>
<TabPanel id="data" label="Data">
{/* <!-- vale off --> */}
| id | title | author | description | fts |
| --- | ------ | ----------------- | -------------------------------- | ------------------------------------------------------- |
| 3 | Tootle | Gertrude Crampton | Little toy train has big dreams. | 'big':5 'dream':6 'littl':1 'tootl':7 'toy':2 'train':3 |
{/* <!-- vale on --> */}
</TabPanel>
</Tabs>
@@ -6,7 +6,7 @@ description: 'How to change your database timezone.'
---
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
### Change timezone
@@ -6,10 +6,10 @@ footerHelpType: 'postgres'
There are 5 options for foreign key constraint deletes:
1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child table(s) are deleted as well.
2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child table(s).
3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to NULL.
4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to their default values.
1. **CASCADE:** When a row is deleted from the parent table, all related rows in the child tables are deleted as well.
2. **RESTRICT:** When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child tables.
3. **SET NULL:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to NULL.
4. **SET DEFAULT:** When a row is deleted from the parent table, the values of the foreign key columns in the child tables are set to their default values.
5. **NO ACTION:** This option is similar to RESTRICT, but it also has the option to be “deferred” to the end of a transaction. This means that other cascading deletes can run first, and then this delete constraint will only throw an error if there is referenced data remaining _at the end of the transaction_.
These options can be specified when defining a foreign key constraint using the "ON DELETE" clause. For example, the following SQL statement creates a foreign key constraint with the `CASCADE` option:
@@ -20,7 +20,7 @@ add constraint fk_parent foreign key (parent_id) references parent_table (id)
on delete cascade;
```
This means that when a row is deleted from the "parent_table", all related rows in the "child_table" will be deleted as well.
This means that when a row is deleted from the `parent_table`, all related rows in the `child_table` will be deleted as well.
## `RESTRICT` vs `NO ACTION`
@@ -28,11 +28,11 @@ The difference between `NO ACTION` and `RESTRICT` is subtle and can be a bit con
Both `NO ACTION` and `RESTRICT` are used to prevent deletion of a row in a parent table if there are related rows in a child table. However, there is a subtle difference in how they behave.
When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced table(s).
When a foreign key constraint is defined with the option `RESTRICT`, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced tables.
When a foreign key constraint is defined with the option `NO ACTION`, it means that if a row in the parent table is deleted, the database will also raise an error and prevent the deletion of the row in the parent table. However unlike `RESTRICT`, `NO ACTION` has the option defer the check using `INITIALLY DEFERRED`. This will only raise the above error _if_ the referenced rows still exist at the end of the transaction.
The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced table(s).
The difference from `RESTRICT` is that a constraint marked as `NO ACTION INITIALLY DEFERRED` is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as `CASCADE`, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like `RESTRICT`, the database will not delete, update or set to NULL any rows in the referenced tables.
In practice, you can use either `NO ACTION` or `RESTRICT` depending on your needs. `NO ACTION` is the default behavior if you do not specify anything. If you prefer to defer the check until the end of the transaction, use `NO ACTION INITIALLY DEFERRED`.
@@ -48,10 +48,10 @@ Let's further illustrate the difference with an example. We'll use the following
`parent`
| id | name | parent_id |
| --- | ------- | --------- |
| 1 | Charles | 1 |
| 2 | Diana | 1 |
| id | name | `parent_id` |
| --- | ------- | ----------- |
| 1 | Charles | 1 |
| 2 | Diana | 1 |
`child`
@@ -36,7 +36,7 @@ This data can further be used in conjunction with the [`explain`](https://www.po
## Managing timezones
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time."
### Change timezone
@@ -38,11 +38,11 @@ Your Postgres database is the core of your Supabase project, so it's important t
### Special symbols in passwords
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string, for example, `postgresql://postgres.projectref:p%3Dword@aws-0-us-east-1.pooler.supabase.com:6543/postgres`
### Changing your project password
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to 3rd-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to third-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
## Granting permissions
@@ -86,7 +86,7 @@ The default Postgres role. This has admin privileges.
### `anon`
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
For "anonymous access." This is the role which the API (PostgREST) will use when a user _is not_ logged in.
### `authenticator`
@@ -95,7 +95,7 @@ A special role for the API (PostgREST). It has very limited access, and is used
### `authenticated`
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
For "authenticated access." This is the role which the API (PostgREST) will use when a user _is_ logged in.
### `service_role`
@@ -11,7 +11,7 @@ When you need granular authorization rules, nothing beats Postgres's [Row Level
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/auth/row-level-security) for end-to-end user security from the browser to the database.
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through 3rd party tooling.
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through third-party tooling.
## Policies
@@ -219,7 +219,7 @@ to authenticated
using ( (select auth.uid()) = user_id );
```
This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an initPlan to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
<Admonition type="caution">
@@ -239,7 +239,7 @@ You can only use this technique if the results of the query or function do not c
### Add filters to every query
Policies are "implicit where clauses", so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
Policies are "implicit where clauses," so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example):
{/* prettier-ignore */}
```js
@@ -99,7 +99,7 @@ execute function after_delete_function();
There are two options available for executing triggers:
- `for each row`: specifies that the trigger function should be executed once for each affected row.
- `for each statement`: the trigger is executed once for the entire operation (eg, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
- `for each statement`: the trigger is executed once for the entire operation (for example, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
## Dropping a trigger
@@ -14,7 +14,7 @@ This guide is intended to:
This is not a comprehensive resource, but rather a helpful starting point for your optimization journey.
If you're new to query optimization, you may be interested in [index_advisor](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
If you're new to query optimization, you may be interested in [`index_advisor`](/docs/guides/database/extensions/index_advisor), our tool for automatically detecting indexes that improve performance on a given query.
## Example query
@@ -66,7 +66,7 @@ create index idx_orders_customer_id on orders (customer_id);
Sorting can also be optimized by indexing:
- An index on `b.date_of_purchase` wcan improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
- An index on `b.date_of_purchase` can improve the sorting process, and is particularly beneficial when a subset of rows is being returned with a `limit` clause.
```sql
create index idx_orders_date_of_purchase on orders (date_of_purchase);
+4 -4
View File
@@ -79,7 +79,7 @@ updated_at | 2022-12-14 02:34:23.85159+00
</details>
Alternatively, you can create a secret by `insert`ing data into the `vault.secret` table:
Alternatively, you can create a secret by inserting data into the `vault.secret` table:
{/* prettier-ignore */}
```sql
@@ -206,11 +206,11 @@ updated_at | 2022-12-14 02:51:13.938396+00
></iframe>
</div>
As we mentioned, the Vault uses pgsodium's Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
As we mentioned, the Vault uses `pgsodium`'s Transparent Column Encryption (TCE) to store secrets in an authenticated encrypted form. There are some details around that you may be curious about, what does authenticated mean, and where are encryption keys store? This section explains those details.
### Authenticated encryption with associated data
The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) encryption algorithm (based on libsodium).
The first important feature of TCE is that it uses an [Authenticated Encryption with Associated Data](<https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)>) encryption algorithm (based on `libsodium`).
### Encryption key location
@@ -218,7 +218,7 @@ The first important feature of TCE is that it uses an [Authenticated Encryption
**Associated Data** means that you can include any other columns from the same row as part of the signature computation. This doesn't encrypt those other columns - rather it ensures that your encrypted value is only associated with columns from that row. If an attacker were to copy an encrypted value from another row to the current one, the signature would be rejected (assuming you used a unique column in the associated data).
Another important feature of pgsodium is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_.
Another important feature of `pgsodium` is that the encryption keys are never stored in the database alongside the encrypted data. Instead, only a **Key ID** is stored, which is a reference to the key that is only accessible outside of SQL. Even if an attacker can capture a dump of your entire database, they will see only encrypted data and key IDs, _never the raw key itself_.
This is an important safety precaution - there is little value in storing the encryption key in the database itself as this would be like locking your front door but leaving the key in the lock! Storing the key outside the database fixes this issue.
@@ -13,7 +13,7 @@ Dart Edge is an experimental project - it's probably not ready for production us
</Admonition>
## Pre requirements
## Prerequisites
Make sure you have [the edge CLI](https://docs.dartedge.dev/#getting-started) installed in addition to [the Supabase CLI](https://supabase.com/docs/guides/cli#installation).
@@ -74,7 +74,7 @@ Now let’s try to interact with our Supabase database. We will start by install
### Add dependencies
Add [supabase](https://pub.dev/packages/supabase) to interact with Supabase service.
Add [`supabase`](https://pub.dev/packages/supabase) to interact with Supabase service.
```bash
dart pub add supabase
@@ -125,14 +125,13 @@ supabase:
project_path: './supabase'
```
Also, currently Dart Edge doesn't work with the latest versions of Flutter. If you do have the latest version of Flutter on your machine, you can use [FVM](https://fvm.app/) to use an older version of Flutter for your Dart Edge project.
Also, currently Dart Edge doesn't work with the latest versions of Flutter. If you do have the latest version of Flutter on your machine, you can use [Flutter Version Management (FVM)](https://fvm.app/) to use an older version of Flutter for your Dart Edge project.
```bash
fvm use 3.7.12 -f
```
You can read more about how to configure FVM (Flutter
Version Management) for your project in the [FVM documentation](https://fvm.app/documentation/getting-started).
You can read more about how to configure FVM for your project in the [FVM documentation](https://fvm.app/documentation/getting-started).
Run the following command to compile your Dart code. This command will start a watcher that will recompile anytime you make changes to your code.
+1 -1
View File
@@ -183,7 +183,7 @@ hideToc: true
})}
</div>
### Web app tutorials
### Web app demos
<div className="grid lg:grid-cols-12 gap-6 not-prose">
{
@@ -29,7 +29,7 @@ Each Supabase project consists of several tools:
### PostgreSQL (database)
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database — you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
PostgreSQL is the core of Supabase. We do not abstract the PostgreSQL database—you can access it and use it with full privileges. We simply provide tools which makes PostgreSQL as easy to use as Firebase.
- Official Docs: [postgresql.org/docs](https://www.postgresql.org/docs/current/index.html)
- Source code: [github.com/postgres/postgres](https://github.com/postgres/postgres) (mirror)
@@ -57,7 +57,7 @@ A JWT-based API for managing users and issuing access tokens. This integrates wi
### PostgREST (API)
A standalone web server that turns your PostgreSQL database directly into a RESTful API.
We use this with our [pg_graphql](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
We use this with our [`pg_graphql`](https://github.com/supabase/pg_graphql) extension to provide a GraphQL API.
- Official Docs: [postgrest.org](https://postgrest.org/)
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
@@ -86,8 +86,8 @@ An S3-compatible object storage service that stores metadata in Postgres.
A modern runtime for JavaScript and TypeScript.
- Official Docs: [deno.land](https://deno.land/)
- Source code: [github.com/denoland/deno](https://github.com/denoland/deno)
- Official Docs: [Deno documentation](https://deno.land/)
- Source code: [Deno source code](https://github.com/denoland/deno)
- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md)
- Language: TypeScript / Rust
@@ -104,14 +104,14 @@ A RESTful API for managing your Postgres. Fetch tables, add roles, and run queri
A cloud-native, multi-tenant Postgres connection pooler.
- Official Docs: [Supavisor Github Pages](https://supabase.github.io/supavisor/)
- Source code: [supabase/supavisor](https://github.com/supabase/supavisor)
- Official Docs: [Supavisor GitHub Pages](https://supabase.github.io/supavisor/)
- Source code: [`supabase/supavisor`](https://github.com/supabase/supavisor)
- License: [Apache 2.0](https://github.com/supabase/supavisor/blob/main/LICENSE)
- Language: Elixir
### Kong (API gateway)
A cloud-native API gateway, built on top of Nginx.
A cloud-native API gateway, built on top of NGINX.
- Official Docs: [docs.konghq.com](https://docs.konghq.com/)
- Source code: [github.com/kong/kong](https://github.com/kong/kong)
@@ -144,7 +144,7 @@ Less, but better.
### Everything is portable
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
We use existing standards to increase portability (like pg_dump and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
We use existing standards to increase portability (like `pg_dump` and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
This forces us compete on experience. We aim to be the best Postgres hosting service.
### Play the long game
@@ -68,7 +68,7 @@ Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
### Serverside Auth helpers
### Server-side Auth helpers
Helpers for implementing user authentication in popular frameworks like [Next.js](/docs/guides/auth/auth-helpers/nextjs) and [SvelteKit](/docs/guides/auth/auth-helpers/sveltekit)
@@ -82,7 +82,7 @@ Build login and registration pages with custom themes. [Docs](/docs/guides/auth/
### Auto-generated REST API
RESTful APIs are autogenerated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
### Auto-generated GraphQL API
@@ -92,6 +92,8 @@ hideToc: true
<StepHikeCompact.Code>
<CH.Code className="min-h-[34rem]">
```ts app/notes/page.tsx
import { createClient } from '@/utils/supabase/server';
@@ -103,6 +105,47 @@ hideToc: true
}
```
```ts utils/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
```
</CH.Code>
</StepHikeCompact.Code>
</StepHikeCompact.Step>
@@ -492,7 +492,7 @@ export class AvatarComponent {
### Add the new widget
And then we can add the widget on top of the **AccountComponent** html template:
And then we can add the widget on top of the **AccountComponent** HTML template:
```html src/app/account.component.html
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
@@ -460,6 +460,8 @@ expo install expo-image-picker
Let's create an avatar for the user so that they can upload a profile photo.
We can start by creating a new component:
{/* <!-- vale off --> */}
```tsx components/Avatar.tsx
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
@@ -593,6 +595,8 @@ const styles = StyleSheet.create({
})
```
{/* <!-- vale on --> */}
### Add the new widget
And then we can add the widget to the Account page:
@@ -138,7 +138,7 @@ export class SupabaseService {
Let's set up a route to manage logins and signups. We'll use Magic Links so users can sign in with their email without using passwords.
Create a **LoginPage** with the `ionic g page login` Ionic CLI command.
> This guide will show the template inline, but the example app will have templateUrls
> This guide will show the template inline, but the example app will have `templateUrl`s
```ts src/app/login/login.page.ts
import { Component, OnInit } from '@angular/core'
@@ -33,7 +33,7 @@ Open Android Studio > New Project > Base Activity (Jetpack Compose).
Create or edit the `local.properties` file at the root (same level as `build.gradle`) of your project.
> **Note**: Do not commit this file to your source control, e.g. by adding it to your `.gitignore` file!
> **Note**: Do not commit this file to your source control, for example, by adding it to your `.gitignore` file!
```kotlin
SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
@@ -75,7 +75,7 @@ val apiKey = BuildConfig.SUPABASE_ANON_KEY
![Gradle dependencies](/docs/img/guides/kotlin/gradle-dependencies.png)
In the `build.gradle` (app) file, add these dependencies then press "Sync now". Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions.
In the `build.gradle` (app) file, add these dependencies then press "Sync now." Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions.
```kotlin
implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version"
File diff suppressed because it is too large. Load diff
@@ -17,12 +17,12 @@ If you get stuck while working through this guide, refer to the [full example on
A Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.
The frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **"sides"**, i.e. the "web side" and the "api side".
The frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **"sides,"** that is, the `web side` and the `api side`.
They are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere.
<Admonition type="note">
Important: When this guide refers to "API", that means the Supabase API and when it refers to "api side", that means the RedwoodJS `api side`.
Important: When this guide refers to "API," that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`.
</Admonition>
@@ -51,7 +51,7 @@ RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.
</Admonition>
Make sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` "sides".
Make sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` "sides."
### Initialize a RedwoodJS app
@@ -428,7 +428,7 @@ export default Account
```
You'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access
logIn, logOut, currentUser, and access the `supabase` authenticate client. We'll use it to get an instance
`logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance
of the supabase client to interact with your API.
### Update home page
@@ -90,7 +90,7 @@ VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
```
The `supabaseClient` will be used in fetch calls to Supabase endpoints from our app. As we'll see below, the client is instrumental in implementing authentication using refine's auth provider methods and CRUD actions with appropriate data provider methods.
The `supabaseClient` will be used in fetch calls to Supabase endpoints from our app. As we'll see below, the client is instrumental in implementing authentication using Refine's auth provider methods and CRUD actions with appropriate data provider methods.
One optional step is to update the CSS file `src/App.css` to make the app look nice.
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/refine-user-management/src/App.css).
@@ -404,7 +404,7 @@ Notice above that, we are using three refine hooks, namely the [`useGetIdentity(
`useForm()`, in contrast, is a data hook that exposes a series of useful objects that serve the edit form. For example, we are grabbing the `onFinish` function to submit the form with the `handleSubmit` event handler. We are also using `formLoading` property to present state changes of the submitted form.
The `useForm()` hook is a higher-level hook built on top of refine's `useForm()` core hook. It fully supports form state management, field validation and submission using React Hook Form. Behind the scenes, it invokes the `dataProvider.getOne` method to get the user profile data from our Supabase `/profiles` endpoint and also invokes `dataProvider.update` method when `onFinish()` is called.
The `useForm()` hook is a higher-level hook built on top of Refine's `useForm()` core hook. It fully supports form state management, field validation and submission using React Hook Form. Behind the scenes, it invokes the `dataProvider.getOne` method to get the user profile data from our Supabase `/profiles` endpoint and also invokes `dataProvider.update` method when `onFinish()` is called.
### Launch!
@@ -117,7 +117,7 @@ Let's set up a Svelte component to manage logins and sign ups. We'll use Magic L
After a user is signed in we can allow them to edit their profile details and manage their account.
Let's create a new component for that called `Account.svelte`.
```html src/lib/Account.svelte
```tsx src/lib/Account.svelte
<script lang="ts">
import { onMount } from 'svelte'
import type { AuthSession } from '@supabase/supabase-js'
@@ -204,8 +204,7 @@ Let's create a new component for that called `Account.svelte`.
{loading ? 'Saving ...' : 'Update profile'}
</button>
</div>
<button type="button" class="button block" on:click="{()" ="">
supabase.auth.signOut()}> Sign Out
<button type="button" class="button block" on:click={() => supabase.auth.signOut()}> Sign Out
</button>
</form>
```
@@ -228,7 +228,7 @@ Add the `Auth` component to your home page
</div>
```
Create a `src/routes/+page.server.ts` file that will return our website url to be used in our `redirectTo` above.
Create a `src/routes/+page.server.ts` file that will return our website URL to be used in our `redirectTo` above.
```ts
// src/routes/+page.server.ts
@@ -119,7 +119,7 @@ For security reasons, Preview Branches do not contain production data. Future ve
### Merging production changes
When you merge your Git branch into the production branch, all changes will be applied to your Production environment.
When you merge your Git branch into the production branch, all new migrations will be applied to your Production environment.
### Git providers
@@ -6,21 +6,29 @@ description: "Learn about your project's instance and additional add-ons."
Every project on the Supabase Platform comes with its own dedicated Postgres instance running inside a virtual machine (VM). The following table describes the base instance with additional compute add-ons available if you need extra performance when scaling up Supabase.
| Plan | Hourly Price USD | Monthly Price USD | CPU | Memory | Connections: Direct | Connections: Pooler |
| ------ | ---------------- | ----------------- | ----------------------- | ------ | ------------------- | ------------------- |
| Micro | $0.01344 | ~$10 | 2-core ARM (shared) | 1 GB | 60 | 200 |
| Small | $0.0206 | ~$15 | 2-core ARM (shared) | 2 GB | 90 | 200 |
| Medium | $0.0822 | ~$60 | 2-core ARM (shared) | 4 GB | 120 | 200 |
| Large | $0.1517 | ~$110 | 2-core ARM (dedicated) | 8 GB | 160 | 300 |
| XL | $0.2877 | ~$210 | 4-core ARM (dedicated) | 16 GB | 240 | 700 |
| 2XL | $0.562 | ~$410 | 8-core ARM (dedicated) | 32 GB | 380 | 1500 |
| 4XL | $1.32 | ~$960 | 16-core ARM (dedicated) | 64 GB | 480 | 3000 |
| 8XL | $2.562 | ~$1,870 | 32-core ARM (dedicated) | 128 GB | 490 | 6000 |
| 12XL | $3.836 | ~$2,800 | 48-core ARM (dedicated) | 192 GB | 500 | 9000 |
| 16XL | $5.12 | ~$3,730 | 64-core ARM (dedicated) | 256 GB | 500 | 12,000 |
| Plan | Hourly Price USD | Monthly Price USD | CPU | Memory | Connections: Direct | Connections: Pooler | Recommended Maximum DB Size |
| ------ | ---------------- | ----------------- | ----------------------- | ------ | ------------------- | ------------------- | --------------------------- |
| Micro | $0.01344 | ~$10 | 2-core ARM (shared) | 1 GB | 60 | 200 | 10GB |
| Small | $0.0206 | ~$15 | 2-core ARM (shared) | 2 GB | 90 | 200 | 50GB |
| Medium | $0.0822 | ~$60 | 2-core ARM (shared) | 4 GB | 120 | 200 | 100GB |
| Large | $0.1517 | ~$110 | 2-core ARM (dedicated) | 8 GB | 160 | 300 | 200GB |
| XL | $0.2877 | ~$210 | 4-core ARM (dedicated) | 16 GB | 240 | 700 | 500GB |
| 2XL | $0.562 | ~$410 | 8-core ARM (dedicated) | 32 GB | 380 | 1500 | 1TB |
| 4XL | $1.32 | ~$960 | 16-core ARM (dedicated) | 64 GB | 480 | 3000 | 2TB |
| 8XL | $2.562 | ~$1,870 | 32-core ARM (dedicated) | 128 GB | 490 | 6000 | 4TB |
| 12XL | $3.836 | ~$2,800 | 48-core ARM (dedicated) | 192 GB | 500 | 9000 | 6TB |
| 16XL | $5.12 | ~$3,730 | 64-core ARM (dedicated) | 256 GB | 500 | 12,000 | 10TB |
Number of connections above are recommended values.
<Admonition type="note" label="Recommended Database Size">
The database sizes recommended for each instance are intended purely as a guide and recommendation to ensure performance is optimal, these sizes are not enforced.
The performance of your database is impacted by many factors, some of them being the resources available to it or the size of the data contained within it. See the [shared responsibility model](https://supabase.com/docs/guides/platform/shared-responsibility-model) for more information.
</Admonition>
We charge hourly for additional compute based on your usage. Read more about [usage-based billing for compute](/docs/guides/platform/org-based-billing#usage-based-billing-for-compute).
Compute sizes can be changed in [the dashboard here by selecting your project](https://supabase.com/dashboard/project/_/settings/addons?panel=computeInstance). [Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
@@ -52,13 +60,13 @@ SSD Disks are attached to your servers and the disk performance depends on the c
[Contact us](https://supabase.com/contact/enterprise) if you require a custom plan.
### Bursting and disk budget
### Bursting and disk IO budget
Smaller compute instances can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. For example, the free tier can burst up to 2,085 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. Your disk budget gets replenished throughout the day.
Smaller compute instances can burst up to their largest throughput and IOPS for 30 minutes in a day. Beyond that, the performance reverts to the baseline. For example, the free tier can burst up to 2,085 Mbps for 30 minutes a day and reverts to the baseline performance of 87 Mbps. Your disk IO budget gets replenished throughout the day.
If you need consistent disk performance, choose the 4XL or larger compute add-on which has the same baseline and maximum disk throughput and IOPS.
If you're unsure of how much throughput or IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://supabase.com/dashboard/project/_/reports). If the `Disk IO % consumed` stat is more than 1%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric goes to 100%, the workload has used up all available disk budget and will revert to baseline performance. Projects that use any disk budget are good candidates for upgrading to a larger compute add-on with higher baseline throughput.
If you're unsure of how much throughput or IOPS your application requires, you can load test your project and inspect these [metrics in the Dashboard](https://supabase.com/dashboard/project/_/reports). If the `Disk IO % consumed` stat is more than 1%, it indicates that your workload has burst beyond the baseline IO throughput during the day. If this metric goes to 100%, the workload has used up all available disk IO budget and will revert to baseline performance. Projects that use any disk IO budget are good candidates for upgrading to a larger compute add-on with higher baseline throughput.
## Postgres replication slots and WAL senders
@@ -1,18 +1,18 @@
---
id: 'exhaust-disk-io'
title: 'High Disk IO Consumption'
description: 'Understand Disk IO, what can deplete your Disk Budget and what you can do about it.'
description: 'Understand Disk IO, what can deplete your Disk IO Budget and what you can do about it.'
---
## Understanding disk IO and disk budget
## Understanding disk IO and disk IO budget
Disk IO refers to two metrics: throughput in Megabits per Second and IOPS which are Input/Output Operations per Second. Depending on the compute add-on of your instance you will have [different baseline performances](https://supabase.com/docs/guides/platform/compute-add-ons#disk-io).
Smaller compute instances can burst and exceed their baseline performance for a short quota of time every day. This quota is represented as your Disk Budget and once your Disk Budget is consumed, your instance reverts back to its baseline performance. You can read more about this under [Bursting and Disk Budget](https://supabase.com/docs/guides/platform/compute-add-ons#bursting-and-disk-budget).
Smaller compute instances can burst and exceed their baseline performance for a short quota of time every day. This quota is represented as your Disk IO Budget and once your Disk IO Budget is consumed, your instance reverts back to its baseline performance. You can read more about this under [Bursting and Disk IO Budget](https://supabase.com/docs/guides/platform/compute-add-ons#bursting-and-disk-io-budget).
## Depleting your disk budget
## Depleting your disk IO budget
Running out of Disk Budget means that your instance is using more disk than its compute add-on can handle and essentially gets throttled. This could have a wide range of implications:
Running out of Disk IO Budget means that your instance is using more disk than its compute add-on can handle and essentially gets throttled. This could have a wide range of implications:
- Response times on requests can increase noticeably
- CPU usage rises noticeably due to IO wait
@@ -20,15 +20,15 @@ Running out of Disk Budget means that your instance is using more disk than its
- Disruption of internal Postgres processes such as [autovacuuming](https://supabase.com/docs/guides/platform/database-size#vacuum-operations)
- Your instance may become unresponsive
## Monitor your disk budget
## Monitor your disk IO budget
To check your Disk Budget on the Supabase Platform, head over to [Database Health in the Reports section](https://supabase.com/dashboard/project/_/reports/database).
To check your Disk IO Budget on the Supabase Platform, head over to [Database Health in the Reports section](https://supabase.com/dashboard/project/_/reports/database).
It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](https://supabase.com/docs/guides/platform/metrics) to learn more.
## Common reasons for high disk IO usage
Most operations on your Supabase project require the disk in some form. Hence, there can be many reasons for high disk usage. Here are some common ones:
Most operations on your Supabase project require disk IO in some form. Hence, there can be many reasons for high disk IO usage. Here are some common ones:
- **High Memory Usage:** Every Supabase project has 1GB of disk allocated for swapping. When your memory usage is high, the operating system might frequently move parts of the memory back and forth of the swap space on the disk.
- **Low Cache Usage:** If your cache hit rate is low, many of your database requests might go straight to the disk. Go to the [Cache Hit Rate Guide](https://supabase.com/docs/guides/platform/performance#hit-rate) to learn more.
@@ -1,71 +1,15 @@
---
title: 'Read Replicas'
description: 'Spin up additional read-only databases for your project.'
subtitle: 'Spin up additional read-only databases for your project'
description: 'Deploy additional read-only databases in multiple regions for your project.'
subtitle: 'Deploy additional read-only databases in multiple regions for your project'
---
<Admonition type="caution" label="Read replicas are only currently available for Early Access for projects on the Pro Plan and above.">
<Admonition type="note" label="Read Replicas are available to organizations on Pro plan and above who have signed up for early access.">
To try out read replicas, [request access here](https://forms.supabase.com/read-replica).
[Request early access here](https://forms.supabase.com/read-replica).
</Admonition>
Read replicas continuously replicate data from a primary database. You can both read and write data on a primary database, but you can _only_ read (for example, use `select` statements), and not write, on a read replica.
| | select | insert | update | delete |
| ------------ | ------ | ------ | ------ | ------ |
| Primary | ✅ | ✅ | ✅ | ✅ |
| Read Replica | ✅ | - | - | - |
When you create a project, the automatically created database is considered the primary database. If you then add read replicas, they will receive and synchronize data from this primary database. Replication is asynchronous. It happens in the background, so transactions on the primary aren't blocked. The time delay between writing data to the primary and the read replica receiving the change is called replication lag.
Read replicas have many applications:
- They reduce read load on the primary database.
- For example, you can use a read replica for complex analytical queries and reserve the primary for user-facing create, update, and delete operations.
- For projects that need to cover a global audience, additional databases can be spun up closer to users in any of our twelve supported regions to reduce latency.
- Read replicas provide data redundancy.
## How are Read Replicas made?
We use a hybrid approach to replicate data from a primary to its read replicas, combining the native methods of streaming replication and file-based log shipping.
### Streaming replication
Postgres generates a Write Ahead Log (WAL) as database changes occur. With streaming replication, these changes stream from the primary to the read replica server. The WAL alone is sufficient to reconstruct the database to its current state.
This replication method is fast, since changes are streamed directly from the primary to the read replica. On the other hand, it faces challenges when the read replica can't keep up with the WAL changes from its primary. This can happen when the read replica is too small, running on degraded hardware, or has a heavier workload running.
To address this, Postgres does provide tunable configuration, like `wal_keep_size`, to adjust the WAL retained by the primary. If the read replica fails to “catch up” before the WAL surpasses the `wal_keep_size` setting, the replication is terminated. Tuning is a bit of an art - the amount of WAL required is variable for every situation.
### File-based log shipping
In this replication method, the primary continuously buffers WAL changes to a local file and then sends the file to the read replica. If multiple read replicas are present, files could also be sent to an intermediary location accessible by all. The read replica then reads the WAL files and applies those changes. There is higher replication lag than streaming replication since the primary buffers the changes locally first. It also means there is a small chance that WAL changes do not reach read replicas if the primary goes down before the file is transferred. In these cases, if the primary fails a replica using streaming replication would (in most cases) be more up-to-date than a replica using file-based log shipping.
### File-based log shipping 🤝 streaming replication
<figure className="max-w-[700px] mx-auto">
<Image
className="hidden dark:block"
alt="How replication is achieved."
src="/docs/img/guides/platform/read-replicas/streaming-replication-dark.png?v=1"
/>
<Image
className="dark:hidden"
alt="How replication is achieved."
src="/docs/img/guides/platform/read-replicas/streaming-replication-light.png?v=1"
/>
<figcaption>How replication is achieved.</figcaption>
</figure>
We bring these two methods together to achieve quick, stable, and reliable replication. Each method addresses the limitations of the other. Streaming replication minimizes replication lag, while file-based log shipping provides a fallback. For file-based log shipping, we use our existing Point In Time Recovery (PITR) infrastructure. We regularly archive files from the primary using [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, and ship the WAL files to S3.
We combine it with streaming replication to reduce replication lag. Once WAL-G files have been synced from S3, read replicas connect to the primary and stream the WAL directly.
## Getting started
<figure className="max-w-[700px] mx-auto">
<Image
className="hidden dark:block"
@@ -81,19 +25,52 @@ We combine it with streaming replication to reduce replication lag. Once WAL-G f
</figure>
Read replicas can be managed from the [infrastructure settings page](https://supabase.com/dashboard/project/_/settings/infrastructure). For now, each project can only spin up to two read replicas. Read replicas also inherit the compute size of their primary database.
Read Replicas are Postgres databases that are continuously synced to a Supabase project's Primary database. You can both read from and write to a Primary database, but you can _only_ read from a Read Replica, such as using `select` statements.
Read replicas offer the following features:
| | select | insert | update | delete |
| ------------ | ------ | ------ | ------ | ------ |
| Primary | ✅ | ✅ | ✅ | ✅ |
| Read Replica | ✅ | - | - | - |
A database is spun up automatically with every Supabase project. This database is the Primary database. You can choose to add Read Replicas to your project and those Replicas will receive and synchronize data from the Primary database.
Replication is asynchronous and happens in the background so transactions on the Primary aren't blocked. The time delay between writing data to the Primary and the Read Replica receiving the change is replication lag.
Read Replicas have many applications:
- They reduce read load on the Primary database.
- For example, you can use a Read Replica for complex analytical queries and reserve the Primary for user-facing create, update, and delete operations.
- For projects that need to cover a global audience, additional databases can be deployed closer to users in any of our twelve supported regions to reduce latency.
- Read Replicas provide data redundancy.
## Prerequisites
A project needs to satisfy these requirements to deploy a Read Replica:
1. Running on AWS.
- Support for projects on Fly.io is coming.
1. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
- Read Replicas are currently placed on the same compute instance as the database to make sure they can keep up with the Primary's activities.
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
1. Running on Postgres 15.
- For projects running on older versions of PostgreSQL, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
## Getting started
Read Replicas can be managed from the [infrastructure settings page](https://supabase.com/dashboard/project/_/settings/infrastructure). For now, each project can only deploy up to two Read Replicas and all Read Replicas inherit the compute size of their Primary database.
Read Replicas offer the following features:
### Dedicated endpoints
Each read replica has its own dedicated database and API endpoints. The database endpoint can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection info**. The API endpoint can be found in [dashboard API settings](https://supabase.com/dashboard/project/_/settings/api) under **Project URL**.
Each Read Replica has its own dedicated database and API endpoints. The database endpoint can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection info**. The API endpoint can be found in [dashboard API settings](https://supabase.com/dashboard/project/_/settings/api) under **Project URL**.
A read replica's API endpoint only supports `GET` requests to the [REST API](https://supabase.com/docs/guides/api). Requests towards other Supabase products, such as Auth, Storage, and Realtime are not able to utilize a read replica nor its API endpoint for now. We are working on adding read replica support for them.
A Read Replica's API endpoint only supports `GET` requests to the [REST API](https://supabase.com/docs/guides/api). Requests towards other Supabase products, such as Auth, Storage, and Realtime, are not able to utilize a Read Replica nor its API endpoint for now. We are working on adding Read Replica support for them.
### Dedicated connection pool
A connection pool through Supavisor is also available for each read replica. Its unique connection string can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection string**.
A connection pool through Supavisor is also available for each Read Replica. Its unique connection string can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection string**.
### Querying through the SQL editor
@@ -112,45 +89,59 @@ A connection pool through Supavisor is also available for each read replica. Its
</figure>
In the SQL editor, you can choose if you want to run the query on the primary or one of the replicas.
In the SQL editor, you can choose if you want to run the query on the Primary or one of the Replicas.
### API load balancer
When read replicas are spun up, an API load balancer becomes available for the project. An endpoint for it is provisioned and can be found [here](https://supabase.com/dashboard/project/_/settings/api).
When Read Replicas are deployed, an API load balancer becomes available for the project. An endpoint for it is provisioned and can be found [here](https://supabase.com/dashboard/project/_/settings/api).
The load balancer uses a round-robin strategy to route `GET` requests to any of the API endpoints available, including that of the primary database. Non-`GET` requests can also be sent through this endpoint. They are immediately redirected to the primary database.
The load balancer uses a round-robin strategy to route `GET` requests to any of the API endpoints available, including that of the Primary database. Non-`GET` requests can also be sent through this endpoint. They are immediately redirected to the Primary database.
Similar to the read replica API endpoints, only requests to the REST API are available for now. Support for other Supabase products will become available once read replica support has been completed for them.
Similar to the Read Replica API endpoints, only requests to the REST API are available for now. Support for other Supabase products will become available once Read Replica support has been completed for them.
When a project no longer has any read replicas, the load balancer and its endpoint are brought down as well.
When a project no longer has any Read Replicas, the load balancer and its endpoint are brought down as well.
### Centralized configuration management
All settings made through the dashboard will be propagated across all databases of a project. This ensures that no read replica get out of sync with its primary database or with other read replicas.
All settings made through the dashboard will be propagated across all databases of a project. This ensures that no Read Replica get out of sync with its Primary database or with other Read Replicas.
## Limitations
## How are Read Replicas made?
### Prerequisites for using read replicas
We use a hybrid approach to replicate data from a Primary to its Read Replicas, combining the native methods of streaming replication and file-based log shipping.
A project needs to satisfy these requirements to spin up a read replica:
### Streaming replication
1. Running on AWS.
- Support for projects on Fly.io will be added at a later date.
1. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
1. Running on Postgres 15.
- For projects running on older versions of PostgreSQL, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
Postgres generates a Write Ahead Log (WAL) as database changes occur. With streaming replication, these changes stream from the Primary to the Read Replica server. The WAL alone is sufficient to reconstruct the database to its current state.
### Read replicas need to be brought down before starting the following procedures
This replication method is fast, since changes are streamed directly from the Primary to the Read Replica. On the other hand, it faces challenges when the Read Replica can't keep up with the WAL changes from its Primary. This can happen when the Read Replica is too small, running on degraded hardware, or has a heavier workload running.
The following procedures require all read replicas for a project to be brought down before they can be started:
To address this, Postgres does provide tunable configuration, like `wal_keep_size`, to adjust the WAL retained by the Primary. If the Read Replica fails to “catch up” before the WAL surpasses the `wal_keep_size` setting, the replication is terminated. Tuning is a bit of an art - the amount of WAL required is variable for every situation.
1. [Upgrading a project](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
1. Performing [data restoration](/docs/guides/platform/backups#pitr-restoration-process).
### File-based log shipping
Once these operations complete, read replicas can be spun back up.
In this replication method, the Primary continuously buffers WAL changes to a local file and then sends the file to the Read Replica. If multiple Read Replicas are present, files could also be sent to an intermediary location accessible by all. The Read Replica then reads the WAL files and applies those changes. There is higher replication lag than streaming replication since the Primary buffers the changes locally first. It also means there is a small chance that WAL changes do not reach Read Replicas if the Primary goes down before the file is transferred. In these cases, if the Primary fails a Replica using streaming replication would (in most cases) be more up-to-date than a Replica using file-based log shipping.
### Compute Add-ons
### File-based log shipping 🤝 streaming replication
In order to avoid replicas being unable to keep up with activity on the primaries they're following, Read Replicas are currently placed on the same Compute Add-on as the primary they're following.
<figure className="max-w-[700px] mx-auto">
<Image
alt="Map view of Primary and Read Replica databases"
src="/docs/img/guides/platform/read-replicas/streaming-replication-dark.png?v=1"
/>
<figcaption>Map view of Primary and Read Replica databases</figcaption>
</figure>
We bring these two methods together to achieve quick, stable, and reliable replication. Each method addresses the limitations of the other. Streaming replication minimizes replication lag, while file-based log shipping provides a fallback. For file-based log shipping, we use our existing Point In Time Recovery (PITR) infrastructure. We regularly archive files from the Primary using [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, and ship the WAL files to S3.
We combine it with streaming replication to reduce replication lag. Once WAL-G files have been synced from S3, Read Replicas connect to the Primary and stream the WAL directly.
## Operations blocked by Read Replicas
### Project upgrades and data restorations
The following procedures require all Read Replicas for a project to be brought down before they can be performed:
1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
1. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process)
These operations need to be completed before Read Replicas can be re-deployed.
+7 -9
View File
@@ -120,7 +120,13 @@ called `supabase_realtime`, and by managing this publication you can control whi
## Examples
<div className="grid md:grid-cols-12 gap-4 not-prose">
{examples.map((x) => (
{[
{
name: 'Multiplayer.dev',
description: 'Mouse movements and chat messages.',
href: 'https://multiplayer.dev',
},
].map((x) => (
<div className="col-span-12" key={x.href}>
<Link href={x.href} passHref>
<GlassPanel title={x.name}>{x.description}</GlassPanel>
@@ -129,14 +135,6 @@ called `supabase_realtime`, and by managing this publication you can control whi
))}
</div>
export const examples = [
{
name: 'Multiplayer.dev',
description: 'Mouse movements and chat messages.',
href: 'https://multiplayer.dev',
},
]
## Resources
Find the source code and documentation in the Supabase GitHub repository.
+2
View File
@@ -4,6 +4,8 @@ description: 'Resources for getting started building with Supabase.'
hideToc: true
---
{/* <!-- vale off --> */}
<div className="flex flex-col gap-12 my-12">
<div>
@@ -57,7 +57,7 @@ You should change these credentials as soon as possible using the [instructions]
### Accessing the APIs
Each of the APIs are available through the the same API gateway:
Each of the APIs are available through the same API gateway:
- REST: `http://<your-ip>:8000/rest/v1/`
- Auth: `http://<your-domain>:8000/auth/v1/`
@@ -243,7 +243,7 @@ Each system can be [configured](../self-hosting#configuration) independently. So
#### Configuring an email server
You will need to use a production-ready SMTP server for sending emails. You can configure the SMTP server by updating the the following environment variables:
You will need to use a production-ready SMTP server for sending emails. You can configure the SMTP server by updating the following environment variables:
```sh .env
SMTP_ADMIN_EMAIL=
@@ -318,8 +318,6 @@ docker rm supabase-analytics
---
{/* Finish with a video. This also appears in the Sidebar via the "tocVideo" metadata */}
## Demo
A minimal setup working on Ubuntu, hosted on Digital Ocean.
@@ -78,7 +78,7 @@ You can achieve the following by providing: `allowedMimeTypes` and `maxFileSize`
const { data, error } = await supabase.storage.createBucket('avatars', {
public: true,
allowedMimeTypes: ['image/*'],
maxFileSize: '1MB',
fileSizeLimit: '1MB',
})
```
+2
View File
@@ -33,11 +33,13 @@
"@supabase/supabase-js": "^2.39.3",
"@tanstack/react-query": "^5.13.4",
"common": "*",
"common-tags": "^1.8.2",
"config": "*",
"framer-motion": "^11.0.3",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"hast-util-has-property": "^2.0.1",
"icons": "*",
"isbot": "^3.6.5",
"jsrsasign": "^11.0.0",
"lodash": "^4.17.21",
@@ -0,0 +1,167 @@
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import matter from 'gray-matter'
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
import { MDXRemote } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
import components from '~/components'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import Layout from '~/layouts/DefaultGuideLayout'
import { isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
export const org = 'supabase'
export const repo = 'terraform-provider-supabase'
export const branch = 'v1.1.3'
export const docsDir = 'docs'
// Each external docs page is mapped to a local page
const pageMap = [
{
remoteFile: 'README.md',
meta: {
title: 'Terraform Provider',
},
useRoot: true,
},
{
slug: 'tutorial',
remoteFile: 'tutorial.md',
meta: {
title: 'Using the Supabase Terraform Provider',
},
},
]
export default function TerraformDocs({
source,
meta,
editLink,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Layout menuId={MenuId.Platform} meta={meta} editLink={editLink}>
<MDXRemote {...source} components={components} />
</Layout>
)
}
/**
* The GitHub repo uses relative links, which don't lead to the right locations
* in docs.
*
* @param url The original link, as written in the Markdown file
* @returns The rewritten link
*/
const urlTransform: UrlTransformFunction = (url: string) => {
try {
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const getBasename = (pathname: string) =>
pathname.endsWith('.md') ? pathname.replace(/\.md$/, '') : pathname
const stripLeadingPrefix = (pathname: string) => pathname.replace(/^\//, '')
const stripLeadingDocs = (pathname: string) => pathname.replace(/^docs\//, '')
const relativePath = stripLeadingPrefix(getBasename(pathname))
const page = pageMap.find(
({ remoteFile, useRoot }) =>
(useRoot && `${relativePath}.md` === remoteFile) ||
(!useRoot && `${stripLeadingDocs(relativePath)}.md` === remoteFile)
)
if (page) {
return 'terraform' + `/${page.slug}` + hash
}
// If we don't have this page in our docs, link to GitHub repo
return `https://github.com/${org}/${repo}/blob/${branch}${pathname}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
/**
* Fetch markdown from external repo and transform links
*/
export const getStaticProps = (async ({ params }) => {
const [slug] = params.slug ?? []
const page = pageMap.find((page) => page.slug === slug)
if (!page) {
throw new Error(`No page mapping found for slug '${slug}'`)
}
const { meta, remoteFile, useRoot } = page
let response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${useRoot ? '' : `${docsDir}/`}${remoteFile}`
)
let content = await response.text()
// Strip out HTML comments
content = content.replace(/<!--.*?-->/, '')
const { content: source, data } = matter(content)
Object.assign(meta, data)
if (!isValidGuideFrontmatter(meta)) {
throw Error('Guide frontmatter is invalid.')
}
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
const mdxSource = await serialize(source, {
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [
remarkGfm,
remarkMkDocsAdmonition,
remarkPyMdownTabs,
[removeTitle, meta.title],
[remarkCodeHike, codeHikeOptions],
],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
})
return {
props: {
source: mdxSource,
meta,
editLink: `${org}/${repo}/blob/${branch}/${docsDir}/${slug}.md`,
},
}
}) satisfies GetStaticProps
export const getStaticPaths = (async () => {
return {
paths: pageMap.map(({ slug }) => ({
params: {
slug: slug ? [slug] : [],
},
})),
fallback: false,
}
}) satisfies GetStaticPaths
@@ -0,0 +1,400 @@
import { codeBlock } from 'common-tags'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import {
CodeBlock,
Heading,
IconCheck,
IconPlusCircle,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
Tabs,
} from 'ui'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import Layout from '~/layouts/DefaultGuideLayout'
import { org, repo, branch, docsDir } from './[[...slug]]'
function ProviderSettings({ schema }: { schema: any }) {
const attributes = schema.block.attributes
const example = codeBlock`
provider "supabase" {
${Object.keys(attributes).map(
(attribute) =>
`${attribute} = ${attributes[attribute].type === 'string' ? `""` : '<value>'}`
)}
}
`
return (
<section aria-labelledby="provider-settings" className="prose max-w-none">
<Heading tag="h2">Provider settings</Heading>
<p>
Use these settings to configure your Supabase provider and authenticate to your Supabase
project.
</p>
<Heading tag="h3">Example usage</Heading>
<CodeBlock className="not-prose">{example}</CodeBlock>
<Heading tag="h3">Details</Heading>
{/* extra div because width restriction doesn't work on table itself */}
<div className="w-full overflow-auto">
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Optional</th>
<th>Sensitive</th>
</tr>
</thead>
<tbody>
{Object.keys(schema.block.attributes).map((attribute) => (
<tr key={attribute}>
<td>{attribute}</td>
<td>
<ReactMarkdown>{attributes[attribute].description}</ReactMarkdown>
</td>
<td>{attributes[attribute].type}</td>
<td className="align-middle">
{attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{attributes[attribute].sensitive && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}
function Resources({ schema }: { schema: any }) {
return (
<section aria-labelledby="resources" className="prose max-w-none">
<Heading tag="h2">Resources</Heading>
<p>You can configure these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((resource) => (
<Tabs.Panel id={resource} label={resource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${resource}" "<label>" {
${Object.keys(schema[resource].block.attributes)
.filter((attribute) => !schema[resource].block.attributes[attribute].computed)
.map(
(attribute) =>
`${attribute} = ${
schema[resource].block.attributes[attribute].type === 'string'
? `""`
: '<value>'
}`
)}
}
`}</CodeBlock>
<Heading tag="h4">Details</Heading>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Required</th>
<th>Optional</th>
<th>Read-only</th>
</tr>
</thead>
<tbody>
{Object.keys(schema[resource].block.attributes).map((attribute) => (
<tr key={attribute}>
<td>
<code>{attribute}</code>
</td>
<td>
<ReactMarkdown>
{schema[resource].block.attributes[attribute].description}
</ReactMarkdown>
</td>
<td>
{schema[resource].block.attributes[attribute].type ?? (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<IconPlusCircle size={14} className="shrink-0" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
<ul>
{Object.keys(
schema[resource].block.attributes[attribute].nested_type.attributes
).map((nestedAttribute) => (
<li key={nestedAttribute}>
{nestedAttribute}
<ul>
<li>
<ReactMarkdown className="*:!m-0">
{
schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].description
}
</ReactMarkdown>
</li>
<li>
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].type ?? 'nested type'}
</li>
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].required && <li>Required</li>}
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].optional && <li>Optional</li>}
{schema[resource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].computed && <li>Read-only</li>}
</ul>
</li>
))}
</ul>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].required && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[resource].block.attributes[attribute].computed && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</Tabs.Panel>
))}
</Tabs>
</section>
)
}
function DataSources({ schema }: { schema: any }) {
return (
<section aria-labelledby="data-sources" className="prose max-w-none">
<Heading tag="h2">Data sources</Heading>
<p>You can read these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((dataSource) => (
<Tabs.Panel id={dataSource} label={dataSource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${dataSource}" "all" {
${Object.keys(schema[dataSource].block.attributes)
.filter(
(attribute) => !schema[dataSource].block.attributes[attribute].computed
)
.map(
(attribute) =>
`${attribute} = ${
schema[dataSource].block.attributes[attribute].type === 'string'
? `""`
: '<value>'
}`
)}
}
`}</CodeBlock>
<Heading tag="h4">Details</Heading>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Type</th>
<th>Required</th>
<th>Optional</th>
<th>Read-only</th>
</tr>
</thead>
<tbody>
{Object.keys(schema[dataSource].block.attributes).map((attribute) => (
<tr key={attribute}>
<td>
<code>{attribute}</code>
</td>
<td>
<ReactMarkdown>
{schema[dataSource].block.attributes[attribute].description}
</ReactMarkdown>
</td>
<td>
{schema[dataSource].block.attributes[attribute].type ?? (
<Popover_Shadcn_>
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<IconPlusCircle size={14} />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
{schema[dataSource].block.attributes[attribute].nested_type
.nesting_mode === 'set' && 'Array of:'}
<ul>
{Object.keys(
schema[dataSource].block.attributes[attribute].nested_type
.attributes
).map((nestedAttribute) => (
<li key={nestedAttribute}>
{nestedAttribute}
<ul>
<li>
<ReactMarkdown className="*:!m-0">
{
schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].description
}
</ReactMarkdown>
</li>
<li>
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].type ?? 'nested type'}
</li>
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].required && <li>Required</li>}
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].optional && <li>Optional</li>}
{schema[dataSource].block.attributes[attribute].nested_type
.attributes[nestedAttribute].computed && <li>Read-only</li>}
</ul>
</li>
))}
</ul>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].required && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].computed && (
<>
<IconCheck className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
</td>
</tr>
))}
</tbody>
</table>
</Tabs.Panel>
))}
</Tabs>
</section>
)
}
export default function PGGraphQLDocs({
meta,
schema,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Layout menuId={MenuId.Platform} meta={meta}>
The Terraform Provider provices access to{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/resources"
rel="noopener noreferrer"
>
resources
</Link>{' '}
and{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/data-sources"
rel="noreferrer noopener"
>
data sources
</Link>
. Resources are infrastructure objects, such as a Supabase project, that you can declaratively
configure. Data sources are sources of information about your Supabase instances.
<ProviderSettings
schema={schema.provider_schemas['registry.terraform.io/supabase/supabase'].provider}
/>
<Resources
schema={schema.provider_schemas['registry.terraform.io/supabase/supabase'].resource_schemas}
/>
<DataSources
schema={
schema.provider_schemas['registry.terraform.io/supabase/supabase'].data_source_schemas
}
/>
</Layout>
)
}
/**
* Fetch JSON schema from external repo
*/
export const getStaticProps = (async () => {
const meta = {
title: 'Terraform Provider reference',
subtitle: 'Resources and data sources available through the Terraform Provider',
}
let response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/schema.json`
)
if (!response.ok) throw Error('Failed to fetch Terraform JSON schema from GitHub')
const schema = await response.json()
return {
props: {
meta,
schema,
},
}
}) satisfies GetStaticProps
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

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