Merge branch 'master' into hi/www-anon-session-id

This commit is contained in:
phamhieu committed 2023-05-08 09:12:08 +07:00
commit daa9d9c9cd
242 files changed
+6764 -477

No files matched your search

+3 -3
View File
@@ -8,10 +8,10 @@ const flatCommonLibSections = flattenSections(commonLibSections)
const clientLibFiles = [
{ fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false },
{ fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true },
{ fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false },
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
{ fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true },
{ fileName: 'supabase_py_v2', label: 'python', version: 'v2', versionSlug: false },
{ fileName: 'supabase_csharp_v0', label: 'csharp', version: 'v0', versionSlug: false },
{ fileName: 'supabase_swift_v0', label: 'swift', version: 'v0', versionSlug: false },
]
export function generateReferencePages() {
-6
View File
@@ -8,7 +8,6 @@ import { memo, useEffect } from 'react'
import Footer from '~/components/Navigation/Footer'
import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState'
import Head from 'next/head'
import { Announcement, AnnouncementCountdown } from 'ui'
const levelsData = {
home: {
@@ -325,11 +324,6 @@ const SiteLayout = ({ children }) => {
<title>Supabase Docs</title>
</Head>
<main>
<div>
<Announcement>
<AnnouncementCountdown />
</Announcement>
</div>
<div className="flex flex-row h-screen">
<NavContainer />
<Container>
+1 -1
View File
@@ -64,7 +64,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
* Send page telemetry on first page load
*/
if (router.isReady) {
handlePageTelemetry(router.asPath)
handlePageTelemetry(router.basePath + router.asPath)
}
}, [router.isReady])
@@ -214,6 +214,7 @@ const customTheme = {
brandAccent: 'hsl(154 54.8% 45.1%)',
brandButtonText: 'white',
// ..
},
},
dark: {
colors: {
@@ -138,7 +138,7 @@ puts token
1. Edit the `secret_gen.rb` file:
- `key_file` = "Path to the private key you downloaded from Apple". It should look like this: `AuthKey_XXXXXXXXXX.p8`.
- `team_id` = "Your Team ID". This is found at the top right of the Apple Developer site (next to your name).
- `team_id` = "Your Team ID". This is found at the Apple Developer website, under Membership details. This is a 10-character alphanumeric string called "Team ID". Alternatively, this can be seen next to your name in the upper right when viewing your Certificates, Identifiers & Profiles.
- `client_id` = "The Service ID of the service you created". This is the `Services ID` you created in the above step `Obtain a Services ID`. If you've lost this ID, you can find it in the Apple Developer Site:
- Go to `Certificates, Identifiers & Profiles`.
- Click `Identifiers` at the left.
@@ -44,7 +44,7 @@ You can then connect your Supabase project to your FlutterFlow project with the
<video width="99%" muted playsInline controls={true}>
<source
src="/docs/videos/guides/integrations/flutterflow/connect-flutterflow-to-supabase.mp4"
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/connect-flutterflow-to-supabase.mp4"
type="video/mp4"
muted
playsInline
@@ -67,7 +67,7 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<video width="99%" muted playsInline controls={true}>
<source
src="/docs/videos/guides/integrations/flutterflow/insert.mp4"
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/insert.mp4"
type="video/mp4"
muted
playsInline
@@ -88,7 +88,7 @@ To query a Supabase table on a ListView:
<video width="99%" muted playsInline controls={true}>
<source
src="/docs/videos/guides/integrations/flutterflow/select.mp4"
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/select.mp4"
type="video/mp4"
muted
playsInline
@@ -132,7 +132,7 @@ Go to your project page on FlutterFlow and follow the steps below to define the
<video width="99%" muted playsInline controls={true}>
<source
src="/docs/videos/guides/integrations/flutterflow/delete.mp4"
src="https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/videos/docs/guides/integrations/flutterflow/delete.mp4"
type="video/mp4"
muted
playsInline
@@ -17,7 +17,7 @@ Supabase charges for certain usage line items in your project. Those line items
- Database Size
- Storage Size
- Egress
- Asset transformations
- Storage Image Transformations
- Realtime concurrent peak connections
- Realtime messages
- Edge function invocations
@@ -89,7 +89,7 @@ And split it into two files (one table for users and one table for items):
- `smallserial` Creates a key using `(id SMALLSERIAL PRIMARY KEY)` (autoincrementing 2-byte integer).
- `serial` Creates a key using `(id SERIAL PRIMARY KEY)` (autoincrementing 4-byte integer).
- `bigserial` Creates a key using `(id BIGSERIAL PRIMARY KEY)` (autoincrementing 8-byte integer).
- `uuid` Creates a key using `(id UUID PRIMARY KEY DEFAULT uuid_generate_v4())` (randomly generated UUID).
- `uuid` Creates a key using `(id UUID PRIMARY KEY DEFAULT gen_random_uuid())` (randomly generated UUID).
- `firestore_id` Creates a key using `(id TEXT PRIMARY KEY)` (uses existing `firestore_id` random text as key).
- `[<primary_key_name>]` (optional) Name of primary key. Defaults to "id".
+1 -1
View File
@@ -10,7 +10,7 @@ export const meta = {
Supabase Storage provides two methods for uploading files:
- Standard Uploads
- Resuamble Uploads
- Resumable Uploads
## Standard Upload
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -60,6 +60,14 @@ export async function fetchSources() {
'../../spec/common-client-libs-sections.json'
)
const swiftLibReferenceSource = new ClientLibReferenceSource(
'swift-lib',
'/reference/swift',
{ title: 'Swift Reference' },
'../../spec/supabase_swift_v0.yml',
'../../spec/common-client-libs-sections.json'
)
const cliReferenceSource = new CliReferenceSource(
'cli',
'/reference/cli',
@@ -87,6 +95,7 @@ export async function fetchSources() {
dartLibReferenceSource,
pythonLibReferenceSource,
cSharpLibReferenceSource,
swiftLibReferenceSource,
cliReferenceSource,
...githubDiscussionSources,
...guideSources,
@@ -206,7 +206,7 @@ as $$
case
when rec is null then null
-- if no primary key exists, use a random uuid
when pkey_cols = array[]::text[] then uuid_generate_v4()
when pkey_cols = array[]::text[] then gen_random_uuid()
else (
select
uuid_generate_v5(
@@ -109,7 +109,7 @@ create table if not exists public.profiles (
comment on table public.profiles is 'Holds all of users profile information';
create table if not exists public.messages (
id uuid not null primary key default uuid_generate_v4(),
id uuid not null primary key default gen_random_uuid(),
profile_id uuid default auth.uid() references public.profiles(id) on delete cascade not null,
content varchar(500) not null,
created_at timestamp with time zone default timezone('utc' :: text, now()) not null
@@ -266,7 +266,7 @@ These don't have a time component, but they don't have in time they make up for
We can generate them in Postgres like this (with `uuid-ossp`):
```sql
SELECT uuid_generate_v4();
SELECT gen_random_uuid();
uuid_generate_v4
--------------------------------------
@@ -58,7 +58,7 @@ Once your instance is up and running, head over to the [SQL Editor](https://app.
```sql
create table if not exists posts (
id uuid default uuid_generate_v4() primary key,
id uuid default gen_random_uuid() primary key,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
title text,
content text
@@ -58,7 +58,7 @@ We will also introduce a `create_new_room` function, which is a [database functi
-- *** Table definitions ***
create table if not exists public.rooms (
id uuid not null primary key default uuid_generate_v4(),
id uuid not null primary key default gen_random_uuid(),
created_at timestamp with time zone default timezone('utc' :: text, now()) not null
);
comment on table public.rooms is 'Holds chat rooms';
@@ -136,7 +136,7 @@ Let's see how we can use the power of SQL together with the ease-of-use of NoSQL
```sql
CREATE TABLE calendar (
id uuid DEFAULT uuid_generate_v4() NOT NULL,
id uuid DEFAULT gen_random_uuid() NOT NULL,
date date,
user_id uuid NOT NULL,
weight numeric,
@@ -113,10 +113,8 @@ returns table (
content text,
similarity float
)
language plpgsql
language sql stable
as $$
begin
return query
select
documents.id,
documents.content,
@@ -125,7 +123,6 @@ begin
where 1 - (documents.embedding <=> query_embedding) > match_threshold
order by similarity desc
limit match_count;
end;
$$;
```
@@ -0,0 +1,933 @@
---
title: 'Securing your Flutter apps with Multi-Factor Authentication'
description: Build a Flutter app where the user is required to authenticate using Multi-Factor Authentication.
author: tyler_shukert
image: flutter-mfa/flutter-mfa-thumb-og.png
thumb: flutter-mfa/flutter-mfa-thumb.jpg
tags:
- flutter
date: '2023-05-04'
toc_depth: 2
---
Multi-factor authentication or MFA is an essential part of security for any kind of app.
We will take a look at an example app where a user has to sign in via MFA in order to view the contents of the app to demonstrate how easy it is to get started with MFA on Flutter.
## What is Multi-Factor Authentication?
Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), is an additional security layer on top of traditional login methods such as email and password login.
There are several forms of MFA, such as with an SMS or through using an authenticator app such as Google Authenticator. It is considered a best practice to use MFA whenever possible because it protects users against weak passwords or compromised social accounts.
## Why Multi-Factor Authentication matters for Flutter apps
In the context of Flutter apps, MFA is important because it helps protect sensitive user data and prevent unauthorized access to user accounts. By requiring users to provide an additional factor, MFA adds an extra layer of security that makes it harder for attackers to gain access to user accounts.
Given how Flutter is widely used MFA might be a requirement rather than a nice-to-have. Implementing MFA in a Flutter app can improve overall security and give users peace of mind knowing that their data is better protected.
## Building the App
![Multi-factor Authentication with Flutter](/images/blog/flutter-mfa/mfa.png)
We are building a simple app where users register with an email and password. After completing the registration process, the users will be asked to set up MFA using an authenticator app. Once verifying the identity via the authenticator app, the user can go to the home page where they can view the main content.
Login works similarly, where after an email and password login, they are asked to enter the verification code to complete the login process.
The app will have the following directory structure, where `auth` contains any basic auth-related pages, `mfa` contains enrolling and verifying the MFA, and we have some additional pages for us to see that MFA is working correctly.
You can find the complete code created in this article [here](https://github.com/supabase/supabase/tree/master/examples/auth/flutter-mfa).
![Directory Structure of the app](/images/blog/flutter-mfa/directory.png)
### Step 1: Setup the scenes
Let’s start with the `flutter create` command.
```bash
flutter create mfa_app
```
Also, if you do not have a Supabase project yet, create one by heading to [database.new](https://dstabase.new). Within a few minutes, you will have a new Supabase project.
### Step 2: Add the dependencies
Install the [supabase_flutter](https://pub.dev/packages/supabase_flutter) package by running the following command in your terminal.
```bash
dart pub add supabase_flutter
```
Then update your `lib/main.dart` file to initialize Supabase in the main function. You should be able to find your Supabase URL and AnonKey from the `settings -> api` section of your dashboard. We will also extract the `SupabaseClient` for easy access to our Supabase instance.
```dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
await Supabase.initialize(
url: 'SUPABASE_URL',
anonKey: 'SUPABASE_ANONKEY',
);
runApp(const MyApp());
}
/// Extract SupabaseClient instance in a handy variable
final supabase = Supabase.instance.client;
```
Also, add [go_router](https://pub.dev/packages/go_router) to handle our routing and redirects.
```dart
dart pub add go_router
```
We will set up the routes towards the end when we have created all the pages we need. With this, we are ready to jump into creating the app.
Also, if we want to support iOS and Android, we need to set up deep links so that a session can be obtained upon clicking on the confirmation link sent to the user’s email address.
We will configure it so that we can open the app by redirecting to `mfa-app://callback` .
For iOS, open `ios/Runner/info.plist` file and add the following deep link configuration.
```xml
<!-- ... other tags -->
<plist>
<dict>
<!-- ... other tags -->
<!-- Deep Links -->
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>mfa-app</string>
</array>
</dict>
</array>
<!-- Deep Links -->
<!-- ... other tags -->
</dict>
</plist>
```
For Android, open `android/app/src/main/AndroidManifest.xml` file and add the following deep link configuration.
```xml
<manifest ...>
<!-- ... other tags -->
<application ...>
<activity ...>
<!-- ... other tags -->
<!-- Deep Links -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="mfa-app"
android:host="callback" />
</intent-filter>
<!-- END Deep Links -->
</activity>
</application>
</manifest>
```
After, we will add the deep link as one of the redirect URLs in our Supabase dashboard.
Go to `Authentication > URL Configuration` and add `mfa-app://callback/*` as a redirect URL. Make sure you don’t add any extra slashes or anything because if you do, deep linking will not work properly.
![Supabase Dashboard](/images/blog/flutter-mfa/supabase-dashboard.png)
Lastly, we will add the `flutter_svg` package. This package will later be used to display a QR code to scan with their authentication app.
```bash
dart pub add flutter_svg
```
That is all the dependencies that we need. Let’s dive into coding!
### Step 3: Create the signup flow
![Register flow](/images/blog/flutter-mfa/register.png)
Let’s first create a signup flow. Again, the user will register with the app using email and password, and after confirming their email address, they will enroll in MFA using an authenticator app.
The register page contains a form with an email and password field for the user to create a new account. We are just calling the [.signUp()](https://supabase.com/docs/reference/dart/auth-signup) method with it.
As you can see in the code below at `emailRedirectTo` option of the `.signUp()`method, upon clicking on the confirmation link sent to the user, they will be taken to MFA enrollment page, which we will implement later.
Create a `lib/pages/auth/signup_page.dart` file and add the following. There will be some errors, but that is because we haven’t created some of the files yet. The errors will go away as we move on, so ignore them for now.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/auth/login_page.dart';
import 'package:mfa_app/pages/mfa/enroll_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class RegisterPage extends StatefulWidget {
static const route = '/auth/register';
const RegisterPage({super.key});
@override
State<RegisterPage> createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register')),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
label: Text('Email'),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
label: Text('Password'),
),
obscureText: true,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
try {
setState(() {
_isLoading = true;
});
final email = _emailController.text.trim();
final password = _passwordController.text.trim();
await supabase.auth.signUp(
email: email,
password: password,
emailRedirectTo:
'mfa-app://callback${MFAEnrollPage.route}', // redirect the user to setup MFA page after email confirmation
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Check your inbox.')));
}
} on AuthException catch (error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.message)));
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unexpected error occured')));
}
if (mounted) {
setState(() {
_isLoading = false;
});
}
},
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
child: Center(
child: CircularProgressIndicator(color: Colors.white)),
)
: const Text('Register'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.push(LoginPage.route),
child: const Text('I already have an account'),
)
],
),
);
}
}
```
We can then create the enrollment page for MFA. This page is taking care of the following.
- Retrieve the enrollment secret from the server via `supabase.auth.mfa.enroll()` method.
- Displaying the secret and its QR code representation and prompts the user to add the app to their authenticator app
- Verifies the user with a TOTP
The QR code and the secret will be displayed automatically when the page loads. When the user enters the correct 6-digit TOTP, they will be automatically redirected to the home page.
Create `lib/pages/mfa/enroll_page.dart` file and add the following.
```dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/auth/register_page.dart';
import 'package:mfa_app/pages/home_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class MFAEnrollPage extends StatefulWidget {
static const route = '/mfa/enroll';
const MFAEnrollPage({super.key});
@override
State<MFAEnrollPage> createState() => _MFAEnrollPageState();
}
class _MFAEnrollPageState extends State<MFAEnrollPage> {
final _enrollFuture = supabase.auth.mfa.enroll();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Setup MFA'),
actions: [
TextButton(
onPressed: () {
supabase.auth.signOut();
context.go(RegisterPage.route);
},
child: Text(
'Logout',
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
),
),
],
),
body: FutureBuilder(
future: _enrollFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final response = snapshot.data!;
final qrCodeUrl = response.totp.qrCode;
final secret = response.totp.secret;
final factorId = response.id;
return ListView(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 24,
),
children: [
const Text(
'Open your authentication app and add this app via QR code or by pasting the code below.',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
SvgPicture.string(
qrCodeUrl,
width: 150,
height: 150,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Text(
secret,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
IconButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: secret));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Copied to your clip board')));
},
icon: const Icon(Icons.copy),
),
],
),
const SizedBox(height: 16),
const Text('Enter the code shown in your authentication app.'),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
hintText: '000000',
),
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
onChanged: (value) async {
if (value.length != 6) return;
// kick off the verification process once 6 characters are entered
try {
final challenge =
await supabase.auth.mfa.challenge(factorId: factorId);
await supabase.auth.mfa.verify(
factorId: factorId,
challengeId: challenge.id,
code: value,
);
await supabase.auth.refreshSession();
if (mounted) {
context.go(HomePage.route);
}
} on AuthException catch (error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.message)));
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Unexpected error occured')));
}
},
),
],
);
},
),
);
}
}
```
### Step 4: Creating the login flow
![Login flow](/images/blog/flutter-mfa/login.png)
Now that we have created a registration flow, we can get to the login flow for returning existing users. Again, the login page has nothing fancy going. We are just collecting the user’s email and password, and calling the good old [.signInWithPassword()](https://supabase.com/docs/reference/dart/auth-signinwithpassword) method. Upon signing in, the user will be taken to a verify page where the user will then enter their verification code from their authenticator app.
Create `lib/pages/auth/login_page.dart` and add the following.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/mfa/verify_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class LoginPage extends StatefulWidget {
static const route = '/auth/login';
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
label: Text('Email'),
),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
label: Text('Password'),
),
obscureText: true,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
try {
final email = _emailController.text.trim();
final password = _passwordController.text.trim();
await supabase.auth.signInWithPassword(
email: email,
password: password,
);
if (mounted) {
context.go(MFAVerifyPage.route);
}
} on AuthException catch (error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.message)));
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unexpected error occured')));
}
},
child: const Text('Login'),
),
],
),
);
}
}
```
Once a returning user logs in, they are taken to the verification page where they are asked to enter the TOTP from their authenticator app.
This verification page has the same text field as the enrollment page, and upon entering the code, they are taken to the home page.
Create a `lib/pages/mfa/verify_page.dart` file and add the following.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/auth/register_page.dart';
import 'package:mfa_app/pages/home_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class MFAVerifyPage extends StatefulWidget {
static const route = '/mfa/verify';
const MFAVerifyPage({super.key});
@override
State<MFAVerifyPage> createState() => _MFAVerifyPageState();
}
class _MFAVerifyPageState extends State<MFAVerifyPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verify MFA'),
actions: [
TextButton(
onPressed: () {
supabase.auth.signOut();
context.go(RegisterPage.route);
},
child: Text(
'Logout',
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),
),
),
],
),
body: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 24,
),
children: [
Text(
'Verification Required',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
const Text('Enter the code shown in your authentication app.'),
const SizedBox(height: 16),
TextFormField(
decoration: const InputDecoration(
hintText: '000000',
),
style: const TextStyle(fontSize: 24),
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
onChanged: (value) async {
if (value.length != 6) return;
// kick off the verification process once 6 characters are entered
try {
final factorsResponse = await supabase.auth.mfa.listFactors();
final factor = factorsResponse.totp.first;
final factorId = factor.id;
final challenge =
await supabase.auth.mfa.challenge(factorId: factorId);
await supabase.auth.mfa.verify(
factorId: factorId,
challengeId: challenge.id,
code: value,
);
await supabase.auth.refreshSession();
if (mounted) {
context.go(HomePage.route);
}
} on AuthException catch (error) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(error.message)));
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unexpected error occured')));
}
},
),
],
),
);
}
}
```
### Step 5: Add a home page with secure contents
![Home Page](/images/blog/flutter-mfa/home.png)
The home page is where the “secure” contents are displayed. We will create a dummy table with some dummy secure contents for demonstration purposes.
First, we create dummy content. Run the following SQL to create the table and add some content.
```sql
-- Dummy table that contains "secure" information
create table if not exists public.private_posts (
id int generated by default as identity primary key,
content text not null
);
-- Dmmy "secure" data
insert into public.private_posts
(content)
values
('Flutter is awesome!'),
('Supabase is awesome!'),
('Postgres is awesome!');
```
Now, we can add some [row security policy](https://supabase.com/docs/guides/auth/row-level-security) to lock those data down so that only users who have signed in using MFA can view them.
Run the following SQL to secure our data from malicious users.
```sql
-- Enable RLS for private_posts table
alter table public.private_posts enable row level security;
-- Create a policy that only allows read if they user has signed in via MFA
create policy "Users can view private_posts if they have signed in via MFA"
on public.private_posts
for select
to authenticated
using (auth.jwt()->>'aal' = 'aal2');
```
`aal` here stands for [Authenticator Assurance Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), and it will be `aal1` for users who have only signed in with 1 sign-in method, and `aal2` for users who have completed the MFA flow. Checking the `aal` inside RLS policy ensures that the data cannot be viewed by users unless they complete the entire MFA flow.
The nice thing about RLS is that it gives us the flexibility to control how users can interact with the data. In this particular example, we are mandating MFA to view the data, but you could easily create layered permissions where for example a user can view the data with 1 factor, but can edit the data when signed in with MFA. You can see more examples in our official MFA guide [here](https://supabase.com/docs/guides/auth/auth-mfa).
Now that we have the secure data in our Supabase instance, all we need to do is to display them in the HomePage. We can simply query the table and display it using a `FutureBuilder`.
Create a `lib/pages/home_page.dart` file and add the following.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/auth/register_page.dart';
import 'package:mfa_app/pages/list_mfa_page.dart';
class HomePage extends StatelessWidget {
static const route = '/';
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final privatePostsFuture =
supabase.from('private_posts').select<List<Map<String, dynamic>>>();
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: const Text('Unenroll MFA'),
onTap: () {
context.push(ListMFAPage.route);
},
),
PopupMenuItem(
child: const Text('Logout'),
onTap: () {
supabase.auth.signOut();
context.go(RegisterPage.route);
},
),
];
},
)
],
),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: privatePostsFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
// Display the secure private content upon retrieval
final data = snapshot.data!;
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(title: Text(data[index]['content']));
},
);
},
),
);
}
}
```
Because we have set the RLS policy, any user without going through the MFA flow will not see anything on this page.
One final page to add here is the **unenrollment** page. On this page, users can remove any factors that they have added. Once a user removes the factor, the user’s account will no longer be associated with the authenticator app, and they would have to go through the enrollment steps again.
Create `lib/pages/list_mfa_page.dart` file and add the following.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/main.dart';
import 'package:mfa_app/pages/auth/register_page.dart';
/// A page that lists the currently signed in user's MFA methods.
///
/// The user can unenroll the factors.
class ListMFAPage extends StatelessWidget {
static const route = '/list-mfa';
ListMFAPage({super.key});
final _factorListFuture = supabase.auth.mfa.listFactors();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('List of MFA Factors')),
body: FutureBuilder(
future: _factorListFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final response = snapshot.data!;
final factors = response.all;
return ListView.builder(
itemCount: factors.length,
itemBuilder: (context, index) {
final factor = factors[index];
return ListTile(
title: Text(factor.friendlyName ?? factor.factorType.name),
subtitle: Text(factor.status.name),
trailing: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
'Are you sure you want to delete this factor? You will be signed out of the app upon removing the factor.',
),
actions: [
TextButton(
onPressed: () {
context.pop();
},
child: const Text('cancel'),
),
TextButton(
onPressed: () async {
await supabase.auth.mfa.unenroll(factor.id);
await supabase.auth.signOut();
if (context.mounted) {
context.go(RegisterPage.route);
}
},
child: const Text('delete'),
),
],
);
});
},
icon: const Icon(Icons.delete_outline),
),
);
},
);
},
),
);
}
}
```
### Step 6: Putting the pieces together with go_router
Now that we have all the pages, it’s time to put it all together with the help of [go_router](https://pub.dev/packages/go_router).
go_router, as you may know, is a routing package for Flutter, and its redirect feature is particularly helpful for implementing the complex requirement this app had. Particularly, we wanted to make sure that a user who has not yet set up MFA is redirected to the MFA setup page, and only users who have signed in land on the home page.
Another helpful feature of go_router comes when using deep links, and it automatically redirects the users to the correct path of the deep link. Because of this, we can ensure that user lands on the MFA setup page upon confirming their email address.
We will add the router in our `lib/main.dart` file. Your `main.dart` file should now look like this.
```dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mfa_app/pages/auth/login_page.dart';
import 'package:mfa_app/pages/auth/register_page.dart';
import 'package:mfa_app/pages/home_page.dart';
import 'package:mfa_app/pages/list_mfa_page.dart';
import 'package:mfa_app/pages/mfa/verify_page.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:mfa_app/pages/mfa/enroll_page.dart';
void main() async {
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_ANON_KEY',
);
runApp(const MyApp());
}
/// Extract SupabaseClient instance in a handy variable
final supabase = Supabase.instance.client;
final _router = GoRouter(
routes: [
GoRoute(
path: HomePage.route,
builder: (context, state) => const HomePage(),
),
GoRoute(
path: ListMFAPage.route,
builder: (context, state) => ListMFAPage(),
),
GoRoute(
path: LoginPage.route,
builder: (context, state) => const LoginPage(),
),
GoRoute(
path: RegisterPage.route,
builder: (context, state) => const RegisterPage(),
),
GoRoute(
path: MFAEnrollPage.route,
builder: (context, state) => const MFAEnrollPage(),
),
GoRoute(
path: MFAVerifyPage.route,
builder: (context, state) => const MFAVerifyPage(),
),
],
redirect: (context, state) async {
// Any users can visit the /auth route
if (state.location.contains('/auth') == true) {
return null;
}
final session = supabase.auth.currentSession;
// A user without a session should be redirected to the register page
if (session == null) {
return RegisterPage.route;
}
final assuranceLevelData =
supabase.auth.mfa.getAuthenticatorAssuranceLevel();
// The user has not setup MFA yet, so send them to enroll MFA page.
if (assuranceLevelData.currentLevel == AuthenticatorAssuranceLevels.aal1) {
await supabase.auth.refreshSession();
final nextLevel =
supabase.auth.mfa.getAuthenticatorAssuranceLevel().nextLevel;
if (nextLevel == AuthenticatorAssuranceLevels.aal2) {
// The user has already setup MFA, but haven't login via MFA
// Redirect them to the verify page
return MFAVerifyPage.route;
} else {
// The user has not yet setup MFA
// Redirect them to the enrollment page
return MFAEnrollPage.route;
}
}
// The user has signed invia MFA, and is allowed to view any page.
return null;
},
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'MFA App',
debugShowCheckedModeBanner: false,
theme: ThemeData.light().copyWith(
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
),
routerConfig: _router,
);
}
}
```
## Conclusions and future iterations
We looked at how to incorporate Multi-Factor Authentication into a Flutter app with a complete enrollment and verification flow for new and existing users. We saw how we are able to control how the users can interact with the data using their MFA status.
Another common use case is to make MFA optional and allow the user to opt-in whenever they are ready. Optionally enrolling in MFA will require some tweaks in the code, but might be a fun one to try out.
## More Flutter content
- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app)
- [Flutter Authentication and Authorization with RLS](https://supabase.com/blog/flutter-authentication-and-authorization-with-rls)
- [How to build a real-time multiplayer game with Flutter Flame](https://supabase.com/blog/flutter-real-time-multiplayer-game)
- [Write backend code using Dart Edge](https://supabase.com/docs/guides/functions/dart-edge)
+54
View File
@@ -0,0 +1,54 @@
---
title: Mendable.ai switches from Pinecone to Supabase for PostgreSQL vector embeddings.
name: Mendable.ai
description: How Mendable.ai boosts efficiency and accuracy of chat powered search for documentation using Supabase with pg_vector.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/mendableai.png
logo_inverse: /images/customers/logos/light/mendableai.png
og_image: /images/customers/og/mendableai.jpg
tags:
- supabase
date: '2023-05-05'
company_url: 'https://mendable.ai/'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Backed by', text: 'Y Combinator' }]
about: Mendable.ai is Chat Powered Search for Documentation.
---
[Mendable.ai](http://mendable.ai/) provides a chat-powered search engine for technical documentation. Their AI-powered search tool makes it easier for developers and other technical users to find relevant information in complex documentation. Users can simply ask questions in natural language, and the tool returns the most relevant answers. [Mendable.ai](http://mendable.ai/)'s search engine also provides detailed analytics, which helps teams identify knowledge gaps and areas for improvement in their documentation.
## The Challenge
[Mendable.ai](http://mendable.ai/) needed to store and search through large amounts of vector data to improve the efficiency and accuracy of their similarity search operations for their Chat Powered Search for Documentation. They tried Faiss, Weviate, and Pinecone., but found them to be expensive and not very intuitive, especially when it came to storing metadata along with the vectors.
Why they chose Supabase:
[Mendable.ai](http://mendable.ai/) discovered that Supabase supports pg_vector and found it to be a simple and cost-effective solution. They were impressed with the open-source nature of Supabase, as well as its ability to store metadata alongside the vectors. They also appreciated the intuitive interface and ease of use.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
We tried other vector databases - we tried Faiss, we tried Weviate, we tried Pinecone. We found them to be incredibly expensive and not very intuitive. If you’re just doing vector search they’re great, but if you need to store a bunch of metadata that becomes a huge pain.
</Quote>
## What They Built
Using Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to build a more efficient and accurate search function for their AI chatbot. By storing vector data alongside metadata in Supabase, [Mendable.ai](http://mendable.ai/) was able to quickly and easily search through their customers documentation to find the most relevant responses to queries. They found that Supabase's solution was just as performant as dedicated vector databases, but without the high cost.
## The Results
Thanks to Supabase's pg_vector, [Mendable.ai](http://mendable.ai/) was able to significantly improve the efficiency and accuracy of their Chat Powered Search for Documentation. They were able to build faster and more cost-effectively using Supabase’s open source stack.
## Tech stack
[Mendable.ai](http://mendable.ai/)'s tech stack includes React, Next.js, Express, Vercel, and Supabase.
<Quote img="caleb-peffer.jpg" caption="Caleb Peffer - CEO, Mendable.ai">
We looked at the alternatives and chose Supabase because it’s open source, it’s simpler, and, for all the ways we need use it, Supabase has been just as performant - if not more performant - than the other vector databases.
</Quote>
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data'
import Ticket from './ActualTicket'
import Form from './form'
@@ -22,10 +22,6 @@ export default function Conf({
const [userData, setUserData] = useState<UserData>(defaultUserData)
const [pageState, setPageState] = useState<PageState>(defaultPageState)
useEffect(() => {
localStorage.setItem('isGolden', !!userData?.golden ? 'true' : 'false')
}, [userData])
return (
<ConfDataContext.Provider
value={{
+5 -5
View File
@@ -1,5 +1,5 @@
import React from 'react'
import DevelopersData from 'data/Developers.json'
import { links } from 'data/Developers'
import AnnouncementsData from 'data/Announcements.json'
import { useRouter } from 'next/router'
import Image from 'next/image'
@@ -15,8 +15,8 @@ type Props = {
const Developers = () => {
const { basePath } = useRouter()
const iconSections = Object.values(DevelopersData).map((company: Props) => {
const { text, description, url, icon } = company
const iconSections = links.map((link: Props) => {
const { text, description, url, icon } = link
const content = (
<div className="dark:hover:bg-scale-500 -m-3 flex items-start rounded-lg p-3 transition duration-150 ease-in-out hover:bg-gray-50">
@@ -55,8 +55,8 @@ const Developers = () => {
return (
<div className="grid grid-cols-12">
<nav className="col-span-6 py-8" aria-labelledby="solutionsHeading">
<div className="m-3 grid grid-cols-12 gap-x-8 gap-y-4 py-4 pr-3">{iconSections}</div>
<nav className="col-span-6 py-8" aria-labelledby="developersResources">
<div className="m-3 grid grid-cols-12 gap-4 py-4 pr-3">{iconSections}</div>
</nav>
<div className="col-span-6">
<div className="m-3 mx-6">
+20 -1
View File
@@ -263,6 +263,18 @@ const Nav = () => {
Pricing
</a>
</Link>
<Link href="/docs">
<a
className={[
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
border-b-2 border-transparent p-5 px-1
text-sm font-medium`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
Docs
</a>
</Link>
<Link href="/blog">
<a
className={[
@@ -406,7 +418,14 @@ const Nav = () => {
Pricing
</a>
</Link>
<Link href="/docs">
<a
target="_blank"
className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white"
>
Docs
</a>
</Link>
<Link href="https://github.com/supabase/supabase">
<a
target="_blank"
@@ -12,6 +12,7 @@ export const PricingTableRowDesktop = (props: any) => {
<tr
className="divide-scale-600 dark:divide-scale-400 bg-scale-200"
style={{ borderTop: 'none' }}
id={`${props.sectionId}-desktop`}
>
<th
className="border-b border-scale-600 bg-scale-200 bg-scale-50 dark:bg-scale-200 text-scale-1200 sticky top-[62px] z-10 py-3 pl-6 text-left text-sm font-medium dark:text-white"
@@ -99,7 +100,7 @@ export const PricingTableRowMobile = (props: any) => {
return (
<>
<table className="mt-8 w-full">
<table className="mt-8 w-full" id={`${props.sectionId}-mobile`}>
<caption className="bg-scale-50 dark:bg-dark-900 border-scale-400 border-t px-4 py-3 text-left text-sm font-medium dark:text-white">
<div className="flex items-center gap-2">
{category.icon ? <ProductIcon icon={props.icon} /> : null}
-20
View File
@@ -1,20 +0,0 @@
[
{
"text": "Documentation",
"description": "Everything you need to start building with Supabase.",
"url": "/docs",
"icon": "M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
},
{
"text": "Changelog",
"description": "See the latest updates and product improvements.",
"url": "/changelog",
"icon": "M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
},
{
"text": "Careers",
"description": "Join the Supabase team and get involved.",
"url": "/careers",
"icon": "M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
}
]
+26
View File
@@ -0,0 +1,26 @@
export const links = [
{
text: 'Resources',
description: 'View examples, curated content from the community, migration guides, and more.',
url: '/docs/guides/resources',
icon: 'M19.4994 17.9996V7.50107L19.4864 3L14.0723 3L4.5 3L4.5 18.75M4.5 18.75C4.5 19.9926 5.50736 21 6.75 21H19.5V18.5255L19.4871 16.5H14.0728H6.75C5.50736 16.5 4.5 17.5074 4.5 18.75ZM7.50724 3V9L9.75771 6.45713L12.0072 9V3H7.50724Z',
},
{
text: 'Changelog',
description: 'See the latest updates and product improvements.',
url: '/changelog',
icon: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z',
},
{
text: 'Careers',
description: 'Join the Supabase team and get involved.',
url: '/careers',
icon: 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
},
{
text: 'Open Source',
description: 'We support existing open source tools and communities wherever possible.',
url: '/oss',
icon: 'M10.7607 3.08859C15.7723 2.38592 20.0295 5.94789 20.8525 10.7642C21.5964 15.1122 19.4634 19.1692 15.8778 20.9967L14.0736 16.084C15.129 15.4346 15.8232 14.4223 16.0198 13.0819C16.3702 10.7219 14.7442 8.47426 12.3801 8.25648C10.2228 8.04047 8.33304 9.61601 7.99823 11.7702L7.99761 11.7743C7.73427 13.5096 8.47082 15.1847 9.92827 16.0841L8.12693 21C6.34571 20.1066 4.88125 18.6129 3.97963 16.7387L3.97879 16.7369C3.15872 15.0395 3.07406 13.7081 3.01524 12.783C3.01012 12.7024 3.0052 12.6249 3 12.5506C3.10941 7.30353 6.59243 3.6764 10.7607 3.08859Z',
},
]
+1 -1
View File
@@ -217,7 +217,7 @@
}
},
{
"title": "Asset transformations",
"title": "Image Transformations",
"tooltips": {
"main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations."
},
+25 -7
View File
@@ -14,6 +14,7 @@ import DefaultLayout from '~/components/Layouts/Default'
import { Tabs } from 'ui'
import PostTypes from '~/types/post'
import BlogListItem from '~/components/Blog/BlogListItem'
import { useParams } from '~/hooks/useParams'
export async function getStaticProps() {
const allPostsData = getSortedPosts('_blog', undefined, undefined, '** BLOG PAGE **')
@@ -37,6 +38,9 @@ export async function getStaticProps() {
}
})
// Append 'all' category here
categories.unshift('all')
return {
props: {
blogs: allPostsData,
@@ -46,6 +50,7 @@ export async function getStaticProps() {
}
function Blog(props: any) {
const tag = useParams()?.tag
const [category, setCategory] = useState('all')
const [blogs, setBlogs] = useState(props.blogs)
@@ -57,6 +62,13 @@ function Blog(props: any) {
const shiftedBlogs = [...props.blogs]
shiftedBlogs.shift()
if (category === 'all') {
router.replace('/blog', undefined, { shallow: true, scroll: false })
} else {
router.query.tag = category
router.replace(router, undefined, { shallow: true, scroll: false })
}
setBlogs(
category === 'all'
? shiftedBlogs
@@ -68,11 +80,11 @@ function Blog(props: any) {
}, [category])
useEffect(() => {
return props.categories.unshift('all')
}, [])
if (router.isReady && tag && tag !== 'all') {
setCategory(tag)
}
}, [tag, router.isReady])
// append 'all' category
// const categories = props.categories.push('all')
const meta_title = 'Supabase Blog: Open Source Firebase alternative Blog'
const meta_description = 'Get all your Supabase News on the Supabase blog.'
@@ -116,9 +128,15 @@ function Blog(props: any) {
<div className="mx-auto ">
<div className="grid grid-cols-12">
<div className="col-span-12 lg:col-span-12">
<Tabs scrollable size="medium" onChange={setCategory} defaultActiveId={'all'}>
{props.categories.map((categoryId: string) => (
<Tabs.Panel id={categoryId} key={categoryId} label={categoryId} />
<Tabs
scrollable
size="medium"
onChange={setCategory}
defaultActiveId={'all'}
activeId={category}
>
{props.categories.map((tag: string) => (
<Tabs.Panel id={tag} key={tag} label={tag} />
))}
</Tabs>
</div>
+2 -2
View File
@@ -104,8 +104,8 @@ function CustomerStoriesPage(props: any) {
>
<h1 className="text-scale-1200 mb-3 text-3xl">Customer stories</h1>
<h2 className="text-scale-1100 text-xl">
Discover how Supabase is being used around the world to quickly create outstanding
products and set new industry standards.
Discover case studies on how Supabase is being used around the world to quickly
create outstanding products and set new industry standards.
</h2>
</motion.div>
<div className="mx-auto my-12 md:my-20 grid grid-cols-12 gap-6 not-prose">
-8
View File
@@ -39,7 +39,6 @@ export default function TicketHome({ users }: Props) {
const { query } = useRouter()
const [supabase, setSupabase] = useState<SupabaseClient | null>(null)
const [session, setSession] = useState<Session | null>(null)
const [isGolden, setIsGolden] = useState(false)
const { isDarkMode } = useTheme()
const TITLE = 'Supabase LaunchWeek 7'
@@ -83,10 +82,6 @@ export default function TicketHome({ users }: Props) {
useEffect(() => {
document.body.className = '!dark bg-[#1C1C1C]'
if (typeof window !== 'undefined') {
setIsGolden(localStorage?.getItem('isGolden') === 'true' ?? false)
}
return () => {
document.body.className = isDarkMode ? 'dark' : 'light'
}
@@ -129,9 +124,6 @@ export default function TicketHome({ users }: Props) {
</SectionContainer>
<LW7BgGraphic />
</div>
<div
className={['bg-lw7-gradient absolute inset-0 z-0', isGolden && 'gold'].join(' ')}
/>
</div>
<div className="relative !w-full max-w-[100vw] !px-4 sm:max-w-xl md:max-w-4xl lg:max-w-7xl -mt-48 md:mt-[-460px] z-20 flex flex-col justify-around items-center !py-4 md:!py-8 gap-2 md:gap-4 !mx-auto">
+59 -9
View File
@@ -3,7 +3,7 @@ import Solutions from 'data/Solutions.json'
import { NextSeo } from 'next-seo'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import CTABanner from '~/components/CTABanner'
import DefaultLayout from '~/components/Layouts/Default'
@@ -15,7 +15,7 @@ import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
export default function IndexPage() {
const router = useRouter()
const { basePath } = useRouter()
const { basePath, asPath } = useRouter()
const { isDarkMode } = useTheme()
const [showComputeModal, setShowComputeModal] = useState(false)
const [activeMobileTier, setActiveMobileTier] = useState('Free')
@@ -24,6 +24,27 @@ export default function IndexPage() {
const meta_description =
'Explore Supabase fees and pricing information. Find our competitive pricing tiers, with no hidden pricing. We have generous free tiers for those getting started, and Pay As You Go for those scaling up.'
// Ability to scroll into pricing sections like storage
useEffect(() => {
/**
* As we render a mobile and a desktop row for each item and just display based on screen size, we cannot navigate by simple id hash
* on both mobile and desktop. To handle both cases, we actually need to check screen size
*/
const hash = asPath.split('#')[1]
if (!hash) return
let device = 'desktop'
if (window.matchMedia('screen and (max-width: 1024px)').matches) {
device = 'mobile'
}
const element = document.querySelector(`#${hash}-${device}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, [asPath])
/**
* @mildtomato same plan metadata is also in /studio dashboard
* would be good if this constant was shared across apps
@@ -42,12 +63,14 @@ export default function IndexPage() {
description: 'Perfect for passion projects & simple websites.',
preface: 'Get started with:',
features: [
'Social OAuth providers',
'Up to 500MB database & 1GB file storage',
'Up to 2GB bandwidth',
'Up to 50MB file uploads',
'Social OAuth providers',
'50,000 monthly active users',
'Up to 500K Edge Function invocations',
'200 concurrent Realtime connections',
'2 million Realtime messages',
'1-day log retention',
'Community support',
],
@@ -66,15 +89,17 @@ export default function IndexPage() {
priceMonthly: 25,
description: 'For production applications with the option to scale.',
features: [
'No project pausing',
'8GB database & 100GB file storage',
'50GB bandwidth',
'5GB file uploads',
'100,000 monthly active users',
'2M Edge Function invocations',
'Daily backups',
'500 concurrent Realtime connections',
'5 million Realtime messages',
'7-day log retention',
'No project pausing',
'Email support',
'Daily backups',
],
scale: 'Additional fees apply for usage and storage beyond the limits above.',
shutdown: '',
@@ -368,7 +393,10 @@ export default function IndexPage() {
</div>
</div>
<div className="sm:py-18 container relative mx-auto px-4 py-16 shadow-sm md:py-24 lg:px-12 lg:pt-32 lg:pb-12">
<div
id="addons"
className="sm:py-18 container relative mx-auto px-4 py-16 shadow-sm md:py-24 lg:px-12 lg:pt-32 lg:pb-12"
>
<div>
<div className="text-center">
<h2 className="text-scale-1200 text-3xl">Easily customizable add-ons</h2>
@@ -441,7 +469,10 @@ export default function IndexPage() {
</div>
</div>
<div className="grid lg:grid-cols-2 gap-8 items-center mt-12 lg:mt-8 max-w-6xl mx-auto">
<div
id="cost-control"
className="grid lg:grid-cols-2 gap-8 items-center mt-12 lg:mt-8 max-w-6xl mx-auto"
>
<div>
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 inline-block dark:bg-scale-400 dark:text-scale-1100 py-0.5 px-2 text-xs mt-2">
Available for Pro plan
@@ -510,41 +541,49 @@ export default function IndexPage() {
category={pricing.database}
tier={'free'}
icon={Solutions['database'].icon}
sectionId="database"
/>
<PricingTableRowMobile
category={pricing.auth}
tier={'free'}
icon={Solutions['authentication'].icon}
sectionId="auth"
/>
<PricingTableRowMobile
category={pricing.storage}
tier={'free'}
icon={Solutions['storage'].icon}
sectionId="storage"
/>
<PricingTableRowMobile
category={pricing.realtime}
tier={'free'}
icon={Solutions['realtime'].icon}
sectionId="realtime"
/>
<PricingTableRowMobile
category={pricing['edge-functions']}
tier={'free'}
icon={Solutions['edge-functions'].icon}
sectionId="edge-functions"
/>
<PricingTableRowMobile
category={pricing.dashboard}
tier={'free'}
icon={pricing.dashboard.icon}
sectionId="dashboard"
/>
<PricingTableRowMobile
category={pricing.security}
tier={'free'}
icon={pricing.security.icon}
sectionId="security"
/>
<PricingTableRowMobile
category={pricing.support}
tier={'free'}
icon={pricing.support.icon}
sectionId="support"
/>
</>
)}
@@ -765,32 +804,43 @@ export default function IndexPage() {
<PricingTableRowDesktop
category={pricing.database}
icon={Solutions['database'].icon}
sectionId="database"
/>
<PricingTableRowDesktop
category={pricing.auth}
icon={Solutions['authentication'].icon}
sectionId="auth"
/>
<PricingTableRowDesktop
category={pricing.storage}
icon={Solutions['storage'].icon}
sectionId="storage"
/>
<PricingTableRowDesktop
category={pricing.realtime}
icon={Solutions['realtime'].icon}
sectionId="realtime"
/>
<PricingTableRowDesktop
category={pricing['edge-functions']}
icon={Solutions['edge-functions'].icon}
sectionId="edge-functions"
/>
<PricingTableRowDesktop
category={pricing.dashboard}
icon={pricing.dashboard.icon}
sectionId="dashboard"
/>
<PricingTableRowDesktop
category={pricing.security}
icon={pricing.security.icon}
sectionId="security"
/>
<PricingTableRowDesktop
category={pricing.support}
icon={pricing.support.icon}
sectionId="support"
/>
<PricingTableRowDesktop category={pricing.support} icon={pricing.support.icon} />
</tbody>
<tfoot>
<tr className="border-scale-200 dark:border-scale-600 border-t">
@@ -833,7 +883,7 @@ export default function IndexPage() {
</div>
</div>
</div>
<div className="border-t">
<div id="faq" className="border-t">
<div className="mx-auto max-w-5xl gap-y-10 gap-x-10 lg:grid-cols-2">
<div className="sm:py-18 mx-auto px-6 py-16 md:py-24 lg:px-16 lg:py-24 xl:px-20">
<h2 className="h3 text-center">Frequently asked questions</h2>
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

@@ -5,35 +5,35 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Angelico de los Reyes at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 15 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-angelico_de_los_reyes-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgres-point-in-time-recovery</guid>
<title>Point in Time Recovery is now available for Pro projects</title>
<link>https://supabase.com/blog/postgres-point-in-time-recovery</link>
<description>We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use.</description>
<pubDate>Thu, 15 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/continuous-postgresql-backup-walg</guid>
<title>Continuous PostgreSQL Backups using WAL-G</title>
<link>https://supabase.com/blog/continuous-postgresql-backup-walg</link>
<description>Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G.</description>
<pubDate>Sat, 01 Aug 2020 22:10:00 +0000</pubDate>
<pubDate>Sun, 02 Aug 2020 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgresql-templates</guid>
<title>What are PostgreSQL Templates?</title>
<link>https://supabase.com/blog/postgresql-templates</link>
<description>What are PostgreSQL templates and what are they used for?</description>
<pubDate>Wed, 08 Jul 2020 22:10:00 +0000</pubDate>
<pubDate>Thu, 09 Jul 2020 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgresql-physical-logical-backups</guid>
<title>Physical vs Logical Backups in PostgreSQL</title>
<link>https://supabase.com/blog/postgresql-physical-logical-backups</link>
<description>What are physical and logical backups in Postgres?</description>
<pubDate>Mon, 06 Jul 2020 22:10:00 +0000</pubDate>
<pubDate>Tue, 07 Jul 2020 02:02:30 +0000</pubDate>
</item>
</channel>
+2 -2
View File
@@ -5,14 +5,14 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Ant Wilson at Supabase</description>
<language>en</language>
<lastBuildDate>Fri, 26 Feb 2021 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Sat, 27 Feb 2021 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-ant_wilson-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/cracking-postgres-interview</guid>
<title>Cracking PostgreSQL Interview Questions</title>
<link>https://supabase.com/blog/cracking-postgres-interview</link>
<description>Understand the top PostgreSQL Interview Questions</description>
<pubDate>Fri, 26 Feb 2021 23:11:00 +0000</pubDate>
<pubDate>Sat, 27 Feb 2021 03:03:30 +0000</pubDate>
</item>
</channel>
+2 -2
View File
@@ -5,14 +5,14 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Bo Lu at Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 14 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Wed, 14 Dec 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-bo_lu-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</guid>
<title>Supabase Wrappers, a Postgres FDW framework written in Rust</title>
<link>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</link>
<description>A framework for building Postgres Foreign Data Wrappers which connects to Stripe, Firebase, Clickhouse, and more.</description>
<pubDate>Wed, 14 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Wed, 14 Dec 2022 03:03:30 +0000</pubDate>
</item>
</channel>
+3 -3
View File
@@ -5,21 +5,21 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Mark Burggraf at Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 23 Nov 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Wed, 23 Nov 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-burggraf-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/sql-or-nosql-both-with-postgresql</guid>
<title>SQL or NoSQL? Why not use both (with PostgreSQL)?</title>
<link>https://supabase.com/blog/sql-or-nosql-both-with-postgresql</link>
<description>How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL</description>
<pubDate>Wed, 23 Nov 2022 23:11:00 +0000</pubDate>
<pubDate>Wed, 23 Nov 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgres-wasm</guid>
<title>Postgres WASM by Snaplet and Supabase</title>
<link>https://supabase.com/blog/postgres-wasm</link>
<description>We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet.</description>
<pubDate>Sun, 02 Oct 2022 22:10:00 +0000</pubDate>
<pubDate>Sun, 02 Oct 2022 02:02:30 +0000</pubDate>
</item>
</channel>
+19
View File
@@ -0,0 +1,19 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>'Postgres | Supabase Blog</title>
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Greg Richardson at Supabase</description>
<language>en</language>
<lastBuildDate>Sun, 05 Feb 2023 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-gregnr-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/openai-embeddings-postgres-vector</guid>
<title>Storing OpenAI embeddings in Postgres with pgvector</title>
<link>https://supabase.com/blog/openai-embeddings-postgres-vector</link>
<description>An example of how to build an AI-powered search engine using OpenAI's embeddings and PostgreSQL.</description>
<pubDate>Sun, 05 Feb 2023 03:03:30 +0000</pubDate>
</item>
</channel>
</rss>
+3 -3
View File
@@ -5,21 +5,21 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Michel Pelletier at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 15 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-michel-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/vault-now-in-beta</guid>
<title>Supabase Vault is now in Beta</title>
<link>https://supabase.com/blog/vault-now-in-beta</link>
<description>A Postgres extension to store encrypted secrets and encrypt data.</description>
<pubDate>Thu, 15 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/transparent-column-encryption-with-postgres</guid>
<title>Transparent Column Encryption with Postgres</title>
<link>https://supabase.com/blog/transparent-column-encryption-with-postgres</link>
<description>Using pgsodium's Transparent Column Encryption to encrypt data and provide your users with row-level encryption.</description>
<pubDate>Wed, 30 Nov 2022 23:11:00 +0000</pubDate>
<pubDate>Wed, 30 Nov 2022 03:03:30 +0000</pubDate>
</item>
</channel>
+13 -6
View File
@@ -5,42 +5,49 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Oliver Rice at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 16 Feb 2023 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 20 Apr 2023 02:02:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-oli_rice-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/whats-new-in-pg-graphql-v1-2</guid>
<title>What's New in pg_graphql v1.2</title>
<link>https://supabase.com/blog/whats-new-in-pg-graphql-v1-2</link>
<description>New Features in the v1.2 release of pg_graphql</description>
<pubDate>Thu, 20 Apr 2023 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/type-constraints-in-65-lines-of-sql</guid>
<title>Type Constraints in 65 lines of SQL</title>
<link>https://supabase.com/blog/type-constraints-in-65-lines-of-sql</link>
<description>Creating validated data types in Postgres</description>
<pubDate>Thu, 16 Feb 2023 23:11:00 +0000</pubDate>
<pubDate>Thu, 16 Feb 2023 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/pg-graphql-v1</guid>
<title>pg_graphql v1.0</title>
<link>https://supabase.com/blog/pg-graphql-v1</link>
<description>Announcing the v1.0 release of pg_graphql</description>
<pubDate>Thu, 15 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</guid>
<title>Supabase Wrappers, a Postgres FDW framework written in Rust</title>
<link>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</link>
<description>A framework for building Postgres Foreign Data Wrappers which connects to Stripe, Firebase, Clickhouse, and more.</description>
<pubDate>Wed, 14 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Wed, 14 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation</guid>
<title>pg_jsonschema: JSON Schema support for Postgres</title>
<link>https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation</link>
<description>Today we're releasing pg_jsonschema, a Postgres extension for JSON validation.</description>
<pubDate>Thu, 18 Aug 2022 22:10:00 +0000</pubDate>
<pubDate>Thu, 18 Aug 2022 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/audit</guid>
<title>Postgres Auditing in 150 lines of SQL</title>
<link>https://supabase.com/blog/audit</link>
<description>PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL.</description>
<pubDate>Mon, 07 Mar 2022 23:11:00 +0000</pubDate>
<pubDate>Mon, 07 Mar 2022 03:03:30 +0000</pubDate>
</item>
</channel>
@@ -5,42 +5,49 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Paul Copplestone at Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 14 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Sun, 30 Apr 2023 02:02:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-paul_copplestone-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgres-pluggable-strorage</guid>
<title>Next steps for Postgres pluggable storage</title>
<link>https://supabase.com/blog/postgres-pluggable-strorage</link>
<description>Exploring history of Postgres pluggable storage and the possibility of landing it in the Postgres core.</description>
<pubDate>Sun, 30 Apr 2023 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</guid>
<title>Supabase Wrappers, a Postgres FDW framework written in Rust</title>
<link>https://supabase.com/blog/postgres-foreign-data-wrappers-rust</link>
<description>A framework for building Postgres Foreign Data Wrappers which connects to Stripe, Firebase, Clickhouse, and more.</description>
<pubDate>Wed, 14 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Wed, 14 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgres-crdt</guid>
<title>pg_crdt - an experimental CRDT extension for Postgres</title>
<link>https://supabase.com/blog/postgres-crdt</link>
<description>Embedding Yjs and Automerge into Postgres for collaborative applications.</description>
<pubDate>Fri, 09 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Fri, 09 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</guid>
<title>Partial data dumps using Postgres Row Level Security</title>
<link>https://supabase.com/blog/partial-postgresql-data-dumps-with-rls</link>
<description>Using RLS to create seed files for local PostgreSQL testing.</description>
<pubDate>Mon, 27 Jun 2022 22:10:00 +0000</pubDate>
<pubDate>Mon, 27 Jun 2022 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgres-as-a-cron-server</guid>
<title>Postgres as a CRON Server</title>
<link>https://supabase.com/blog/postgres-as-a-cron-server</link>
<description>Running repetitive tasks with your Postgres database.</description>
<pubDate>Thu, 04 Mar 2021 23:11:00 +0000</pubDate>
<pubDate>Fri, 05 Mar 2021 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgresql-views</guid>
<title>Postgres Views</title>
<link>https://supabase.com/blog/postgresql-views</link>
<description>Creating and using a view in PostgreSQL.</description>
<pubDate>Tue, 17 Nov 2020 23:11:00 +0000</pubDate>
<pubDate>Wed, 18 Nov 2020 03:03:30 +0000</pubDate>
</item>
</channel>
+3 -3
View File
@@ -5,21 +5,21 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Pavel Borisov at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 15 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-pavel-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/new-in-postgres-15</guid>
<title>What's new in Postgres 15?</title>
<link>https://supabase.com/blog/new-in-postgres-15</link>
<description>Describes the release of Postgres 15, new features and reasons to use it</description>
<pubDate>Thu, 15 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/postgresql-commitfest</guid>
<title>What is PostgreSQL commitfest and how to contribute</title>
<link>https://supabase.com/blog/postgresql-commitfest</link>
<description>A time-tested method for contributing to the core Postgres code</description>
<pubDate>Wed, 26 Oct 2022 22:10:00 +0000</pubDate>
<pubDate>Wed, 26 Oct 2022 02:02:30 +0000</pubDate>
</item>
</channel>
@@ -5,21 +5,21 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Steve Chavez at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 15 Dec 2022 23:11:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 15 Dec 2022 03:03:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-steve_chavez-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgrest-11-prerelease</guid>
<title>PostgREST 11 pre-release</title>
<link>https://supabase.com/blog/postgrest-11-prerelease</link>
<description>Describes new features of PostgREST 11 pre-release</description>
<pubDate>Thu, 15 Dec 2022 23:11:00 +0000</pubDate>
<pubDate>Thu, 15 Dec 2022 03:03:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/roles-postgres-hooks</guid>
<title>Protecting reserved roles with PostgreSQL Hooks</title>
<link>https://supabase.com/blog/roles-postgres-hooks</link>
<description>Using Postgres Hooks to protect functionality in your Postgres database.</description>
<pubDate>Thu, 01 Jul 2021 22:10:00 +0000</pubDate>
<pubDate>Thu, 01 Jul 2021 02:02:30 +0000</pubDate>
</item>
</channel>
+4 -4
View File
@@ -5,28 +5,28 @@
<link>https://supabase.com/blog</link>
<description>Latest Postgres news from Victor at Supabase</description>
<language>en</language>
<lastBuildDate>Thu, 13 Oct 2022 22:10:00 +0000</lastBuildDate>
<lastBuildDate>Thu, 13 Oct 2022 02:02:30 +0000</lastBuildDate>
<atom:link href="https://supabase.com/planetpg-victor-rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgres-full-text-search-vs-the-rest</guid>
<title>Postgres Full Text Search vs the rest</title>
<link>https://supabase.com/blog/postgres-full-text-search-vs-the-rest</link>
<description>Comparing one of the most popular Postgres features against alternatives</description>
<pubDate>Thu, 13 Oct 2022 22:10:00 +0000</pubDate>
<pubDate>Thu, 13 Oct 2022 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/choosing-a-postgres-primary-key</guid>
<title>Choosing a Postgres Primary Key</title>
<link>https://supabase.com/blog/choosing-a-postgres-primary-key</link>
<description>Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out</description>
<pubDate>Wed, 07 Sep 2022 22:10:00 +0000</pubDate>
<pubDate>Wed, 07 Sep 2022 02:02:30 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/seen-by-in-postgresql</guid>
<title>Implementing "seen by" functionality with Postgres</title>
<link>https://supabase.com/blog/seen-by-in-postgresql</link>
<description>Different approaches for tracking visitor counts with PostgreSQL.</description>
<pubDate>Sun, 17 Jul 2022 22:10:00 +0000</pubDate>
<pubDate>Sun, 17 Jul 2022 02:02:30 +0000</pubDate>
</item>
</channel>
+197 -176
View File
File diff suppressed because it is too large. Load diff
+390
View File
@@ -1154,6 +1154,24 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/whats-new-in-pg-graphql-v1-2</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/launch-week-7-hackathon-winners</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/postgres-pluggable-strorage</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/customers/epsilon3</loc>
<changefreq>weekly</changefreq>
@@ -1195,4 +1213,376 @@
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/antstack</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/appsmith</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/arceptive</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/arengu</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/authsignal</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/basedash</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/clerk</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/clutch</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/codesandbox</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/dashibase</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/deepnote</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/dhiwise</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/directus</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/doppler</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/draftbit</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/drapcode</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/enginedigital</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/estuary</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/experts</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/feelantera</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/fezto</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/flutterflow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/getstream_io</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/illa</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/integrations</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/internalio</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/jetadmin</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/keyri</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/morrow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/nextauth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/nuxt</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/onesignal</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/ory</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/passageidentity</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/pgmustard</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/picket</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/plasmic</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/polyscale</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/prisma</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/quinence</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/readyset</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/replibyte</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/replicache</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/retool</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/sequin_io</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/snaplet</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/solid_software</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/streamlit</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/stytch</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/supertokens</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/teta</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/tinloof</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/trevor_io</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/tribehealthsolutions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/uncommonpurpose</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/vercel</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/voltapp</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/voypost</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/weweb</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/yepcode</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/zapp_run</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/partners/zuplo</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
</urlset>
Loaded 100 of 242 files, more files were not shown because too many files have changed in this diff. Show more