Merge branch 'master' into feat/email-auth
No files matched your search
@@ -119,8 +119,8 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase-community/functions-csharp" target="_blank" rel="noopener noreferrer">functions-csharp</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dart (Flutter)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-dart" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
|
||||
@@ -134,7 +134,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
|
||||
@@ -397,147 +397,6 @@ POST https://api.supabase.com/v1/projects
|
||||
```
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
### List all functions {#list-all-functions}
|
||||
|
||||
```
|
||||
GET https://api.supabase.com/v1/projects/{ref}/functions
|
||||
```
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
<ul className="method-list-group">
|
||||
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
ref
|
||||
</span>
|
||||
<span className="method-list-item-label-badge">
|
||||
required
|
||||
</span>
|
||||
<span className="method-list-item-validation">
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="method-list-item-description">
|
||||
Project ref
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
#### Responses
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="200">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"ACTIVE",
|
||||
"REMOVED",
|
||||
"THROTTLED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "number"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "number"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "number"
|
||||
},
|
||||
"verify_jwt": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"slug",
|
||||
"name",
|
||||
"status",
|
||||
"version",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="403">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="500">
|
||||
|
||||
Failed to retrieve project's functions
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
@@ -706,6 +565,147 @@ Failed to create project's function
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
### List all functions {#list-all-functions}
|
||||
|
||||
```
|
||||
GET https://api.supabase.com/v1/projects/{ref}/functions
|
||||
```
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
<ul className="method-list-group">
|
||||
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
ref
|
||||
</span>
|
||||
<span className="method-list-item-label-badge">
|
||||
required
|
||||
</span>
|
||||
<span className="method-list-item-validation">
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="method-list-item-description">
|
||||
Project ref
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
#### Responses
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="200">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"ACTIVE",
|
||||
"REMOVED",
|
||||
"THROTTLED"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "number"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "number"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "number"
|
||||
},
|
||||
"verify_jwt": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"slug",
|
||||
"name",
|
||||
"status",
|
||||
"version",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="403">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="500">
|
||||
|
||||
Failed to retrieve project's functions
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
@@ -1490,6 +1490,112 @@ Failed to delete secrets with given names
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
### Generate TypeScript types {#generate-typescript-types}
|
||||
|
||||
```
|
||||
GET https://api.supabase.com/v1/projects/{ref}/types/typescript
|
||||
```
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
<ul className="method-list-group">
|
||||
|
||||
<li className="method-list-item">
|
||||
<h4 className="method-list-item-label">
|
||||
<span className="method-list-item-label-name">
|
||||
ref
|
||||
</span>
|
||||
<span className="method-list-item-label-badge">
|
||||
required
|
||||
</span>
|
||||
<span className="method-list-item-validation">
|
||||
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
<div class="method-list-item-description">
|
||||
Project ref
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
#### Responses
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="200">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"types": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"types"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="403">
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="500">
|
||||
|
||||
Failed to generate TypeScript types
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
|
||||
|
||||
<!-- AUTOGENERATED: DO NOT EDIT DIRECTLY IF THIS IS VERSION "next" -->
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -7,24 +7,9 @@ slug: initializing
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Dart
|
||||
|
||||
You can initialize a new Supabase client using the `SupabaseClient()` method.
|
||||
|
||||
The Supabase client is your entrypoint to the rest of the Supabase functionality
|
||||
and is the easiest way to interact with everything we offer within the Supabase ecosystem.
|
||||
|
||||
## Flutter
|
||||
|
||||
For `supabase_flutter`, you will be using the static `initialize()` method on `Supabase` class.
|
||||
|
||||
## Examples
|
||||
|
||||
### Dart `SupabaseClient()`
|
||||
|
||||
```dart
|
||||
final supabase = SupabaseClient('https://xyzcompany.supabase.co', 'public-anon-key');
|
||||
```
|
||||
For `supabase-flutter`, you will be using the static `initialize()` method on `Supabase` class.
|
||||
|
||||
### Flutter `initialize()`
|
||||
|
||||
@@ -34,3 +19,25 @@ Future<void> main() async {
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Access `SupabaseClient` instance
|
||||
|
||||
Once you initialize Supabase in your `main()` method, you can access the `SupabaseClient` instance from anywhere in your app.
|
||||
|
||||
```dart
|
||||
final supabase = Supabase.instance.client;
|
||||
```
|
||||
|
||||
## Other Dart Projects
|
||||
|
||||
You can initialize a new Supabase client using the `SupabaseClient()` method.
|
||||
|
||||
The Supabase client is your entrypoint to the rest of the Supabase functionality
|
||||
and is the easiest way to interact with everything we offer within the Supabase ecosystem.
|
||||
|
||||
### Dart `SupabaseClient()`
|
||||
|
||||
```dart
|
||||
final supabase = SupabaseClient('https://xyzcompany.supabase.co', 'public-anon-key');
|
||||
```
|
||||
|
||||
@@ -7,25 +7,18 @@ slug: installing
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
## Dart
|
||||
|
||||
Dart libraries are built and supported by the community.
|
||||
|
||||
```bash
|
||||
dart pub add supabase
|
||||
```
|
||||
|
||||
Find the source code on [GitHub](https://github.com/supabase/supabase-dart).
|
||||
|
||||
## Flutter
|
||||
|
||||
For Flutter project, you can use [supabase_flutter](https://github.com/supabase/supabase-flutter).
|
||||
For Flutter project, you can use [supabase_flutter](https://pub.dev/packages/supabase_flutter) package.
|
||||
|
||||
```bash
|
||||
flutter pub add supabase_flutter
|
||||
```
|
||||
|
||||
`supabase_flutter` plugin uses `supabase` plugin internally, and it adds some Flutter specific functionality such as handling deeplinks coming back from magic link verifications.
|
||||
If you are creating a Flutter application, we recommend using `supabase_flutter` instead of `supabase`.
|
||||
## Other Dart projects
|
||||
|
||||
For the most part `supabase_flutter` shares the same API as `supabase` with few exceptions such as initialization or OAuth sign in.
|
||||
For non-Flutter projects using Dart, such as server-side Dart or Angular-Dart, you can use the [supabase](https://pub.dev/packages/supabase) package.
|
||||
|
||||
```bash
|
||||
dart pub add supabase
|
||||
```
|
||||
@@ -1,22 +1,14 @@
|
||||
---
|
||||
id: intro
|
||||
slug: /
|
||||
sidebar_label: Supabase Dart Library
|
||||
sidebar_label: Supabase Flutter Library
|
||||
---
|
||||
|
||||
# Supabase Dart Library
|
||||
# Supabase Flutter Library
|
||||
|
||||
:::note
|
||||
This reference documents every object and method available in Supabase's isomorphic Flutter library, [`supabase-flutter`](https://pub.dev/packages/supabase_flutter).
|
||||
|
||||
You're viewing the Supabase docs for a developer preview version.
|
||||
|
||||
Refer to the `v0` docs for the previous release.
|
||||
|
||||
:::
|
||||
|
||||
This reference documents every object and method available in Supabase's isomorphic Dart library, `supabase-dart`.
|
||||
|
||||
You can use the `supabase-dart` library to:
|
||||
You can use the `supabase-flutter` library to:
|
||||
|
||||
- interact with your Postgres database
|
||||
- listen to database changes
|
||||
@@ -24,7 +16,12 @@ You can use the `supabase-dart` library to:
|
||||
- build login and user management functionality
|
||||
- manage large files
|
||||
|
||||
## For non-Flutter projects
|
||||
|
||||
We also have [supabase-dart](https://github.com/supabase-community/supabase-dart) for non-Flutter Dart projects, such as server-side Dart or Angular-Dart.
|
||||
supabase-dart shares most of the APIs with supabase-flutter without being dependent on Flutter so that you can use Supabase anywhere you can run Dart!
|
||||
|
||||
## Additional Links
|
||||
|
||||
- Source Code: [github.com/supabase/supabase-dart](https://github.com/supabase/supabase-dart)
|
||||
- Source Code: [github.com/supabase/supabase-flutter](https://github.com/supabase/supabase-flutter)
|
||||
- [Known bugs and issues](https://github.com/supabase/supabase-flutter/issues)
|
||||
@@ -0,0 +1,541 @@
|
||||
---
|
||||
id: upgrade-guide
|
||||
title: Upgrade to supabase-flutter v1
|
||||
description: 'Learn how to upgrade to supabase-flutter v1.'
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
supabase-flutter focuses on improving the developer experience and making it easier to use. This guide will help you upgrade from supabase-flutter v0 to v1.
|
||||
|
||||
## Upgrade the client library
|
||||
|
||||
Update the package in your pubspec.yaml file.
|
||||
|
||||
```yaml
|
||||
supabase_flutter: ^1.0.0
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
The way supabase-flutter throws error has changed in v1. In v0, errors were returned as a response. In v1, errors are thrown as exceptions. This makes it more intuitive as a Flutter developer to handle errors.
|
||||
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final res = await supabase.from('my_table').select().execute();
|
||||
final error = res.error;
|
||||
if(error != null) {
|
||||
// handle error
|
||||
}
|
||||
final data = res.data;
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
try {
|
||||
final data = supabase.from('my_table').select();
|
||||
} catch (error) {
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Auth classes / methods
|
||||
|
||||
### Usage of `SupabaseAuthState` and `SupabaseAuthRequiredState` classes
|
||||
|
||||
In v0, `SupabaseAuthState` and `SupabaseAuthRequiredState` were required to handle automatic token refresh and to listen to auth state change. In v1, `SupabaseAuthState` and `SupabaseAuthRequiredState` are deprecated, and token refresh will happen automatically just by initializing Supabase. [`onAuthStateChange`](/docs/reference/dart/upgrade-guide#listening-to-auth-state-change) can be used to action on auth state change.
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await Supabase.initialize(
|
||||
url: 'SUPABASE_URL',
|
||||
anonKey: 'SUPABASE_ANON_KEY',
|
||||
);
|
||||
...
|
||||
|
||||
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
class AuthRequiredState<T extends StatefulWidget> extends SupabaseAuthState<T> {
|
||||
...
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await Supabase.initialize(
|
||||
url: 'SUPABASE_URL',
|
||||
anonKey: 'SUPABASE_ANON_KEY',
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Listening to auth state change
|
||||
|
||||
`onAuthStateChange` now returns a `Stream`.
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final authSubscription = supabase.auth.onAuthStateChange((event, session) {
|
||||
// handle auth state change
|
||||
});
|
||||
|
||||
// Unsubscribe when no longer needed
|
||||
authSubscription.data?.unsubscribe();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
final authSubscription = supabase.auth.onAuthStateChange.listen((data) {
|
||||
final AuthChangeEvent event = data.event;
|
||||
final Session? session = data.session;
|
||||
// handle auth state change
|
||||
});
|
||||
|
||||
// Unsubscribe when no longer needed
|
||||
authSubscription.cancel();
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Sign in with email and password
|
||||
|
||||
The signIn() method has been deprecated in favor of more explicit method signatures to help with type hinting. Previously it was difficult for developers to know what they were missing (e.g., a lot of developers didn't realize they could use passwordless magic links).
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signIn(email: email, password: password);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithPassword(email: email, password: password);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Sign in with magic link
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signIn(email: email);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithOtp(email: email);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Sign in with a third-party OAuth provider
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithProvider(
|
||||
Provider.github,
|
||||
options: AuthOptions(
|
||||
redirectTo: kIsWeb
|
||||
? null
|
||||
: 'io.supabase.flutter://reset-callback/'),
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithOAuth(
|
||||
Provider.github,
|
||||
redirectTo: kIsWeb ? null : 'io.supabase.flutter://reset-callback/',
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Sign in with phone
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signIn(
|
||||
phone: '+13334445555',
|
||||
password: 'example-password',
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithPassword(
|
||||
phone: '+13334445555',
|
||||
password: 'example-password',
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Sign in with phone using OTP
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final res = await supabase.auth.signIn(phone: phone);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithOtp(
|
||||
phone: phone,
|
||||
);
|
||||
|
||||
// After receiving a SMS with a OTP.
|
||||
await supabase.auth.verifyOTP(
|
||||
type: OtpType.sms,
|
||||
token: token,
|
||||
phone: phone,
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Reset password for email
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.api.resetPasswordForEmail(
|
||||
email,
|
||||
options:
|
||||
AuthOptions(redirectTo: 'io.supabase.flutter://reset-callback/'),
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.resetPasswordForEmail(
|
||||
email,
|
||||
redirectTo: kIsWeb ? null : 'io.supabase.flutter://reset-callback/',
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the user's current session
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final session = supabase.auth.session();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
final Session? session = supabase.auth.currentSession;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the logged-in user
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final user = supabase.auth.user();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
final User? user = supabase.auth.currentUser;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Update user data for a logged-in user
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase.auth.update(
|
||||
UserAttributes(data: {'hello': 'world'})
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.updateUser(
|
||||
UserAttributes(
|
||||
data: { 'hello': 'world' },
|
||||
),
|
||||
);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Data methods
|
||||
|
||||
`.insert()` / `.upsert()` / `.update()` / `.delete()` don't return rows by default.
|
||||
|
||||
Previously, these methods return inserted/updated/deleted rows by default (which caused [some confusion](https://github.com/supabase/supabase/discussions/1548)), and you can opt to not return it by specifying `returning: 'minimal'`. Now the default behavior is to not return rows. To return inserted/updated/deleted rows, add a `.select()` call at the end.
|
||||
|
||||
Also, calling `.execute()` at the end of the query was a requirement in v0, but in v1 `.execute` is deperecated.
|
||||
|
||||
### Insert without returning inserted data
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
await supabase
|
||||
.from('my_table')
|
||||
.insert(data, returning: ReturningOption.minimal)
|
||||
.execute();
|
||||
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.from('my_table').insert(data);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Insert with returning inserted data
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final res = await supabase
|
||||
.from('my_table')
|
||||
.insert(data)
|
||||
.execute();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
final insertedData = await supabase.from('my_table').insert(data).select();
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Realtime methods
|
||||
|
||||
### Stream
|
||||
|
||||
`.stream()` no longer needs the `.execute()` at the end. Also, filtering by `eq` is a lot easier now. `primaryKey` is now a named parameter to make it more obvious what to pass.
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
supabase.from('my_table:id=eq.120')
|
||||
.stream(['id'])
|
||||
.listen();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
supabase.from('my_table')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('id', '120')
|
||||
.listen();
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Subscribe
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
final subscription = supabase
|
||||
.from('countries')
|
||||
.on(SupabaseEventTypes.all, (payload) {
|
||||
// Handle realtime payload
|
||||
})
|
||||
.subscribe();
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
final channel = supabase.channel('*');
|
||||
channel.on(
|
||||
RealtimeListenTypes.postgresChanges,
|
||||
ChannelFilter(event: '*', schema: '*'),
|
||||
(payload, [ref]) {
|
||||
// Handle realtime payload
|
||||
},
|
||||
).subscribe();
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Unsubscribe
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '0.x'},
|
||||
{label: 'After', value: '1.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="0.x">
|
||||
|
||||
```dart
|
||||
supabase.removeSubscription(subscription);
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```dart
|
||||
await supabase.removeChannel(channel);
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -6,6 +6,11 @@ sidebar_label: Supabase Dart Library
|
||||
|
||||
# Supabase Dart Library
|
||||
|
||||
:::caution
|
||||
You're viewing the docs for an older version of the `supabase-flutter` library.
|
||||
:::
|
||||
|
||||
|
||||
This reference documents every object and method available in Supabase's isomorphic Dart library, `supabase-dart`.
|
||||
|
||||
You can use the `supabase-dart` library to:
|
||||
|
||||
@@ -20,6 +20,7 @@ const supabase = createClient(apiURL, apiKey, {
|
||||
schema: 'public',
|
||||
},
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true,
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
---
|
||||
id: upgrade-guide
|
||||
title: Upgrade to supabase-js v2
|
||||
description: 'Learn how to upgrade to supabase-js v2.'
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
supabase-js v2 focuses on "quality-of-life" improvements for developers and addresses some of the largest pain points in v1. v2 includes type support, a rebuilt Auth library with async methods, improved errors, and more.
|
||||
|
||||
No new features will be added to supabase-js v1 , but we'll continuing merging security fixes to v1, with maintenance patches for the next 3 months.
|
||||
|
||||
## Upgrade the client library
|
||||
|
||||
|
||||
```sh
|
||||
npm install @supabase/supabase-js@2
|
||||
```
|
||||
|
||||
*Optionally* if you are using custom configuration with `createClient` then follow below:
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts title="src/supabaseClient.ts"
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
schema: 'custom',
|
||||
persistSession: false
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts title="src/supabaseClient.ts"
|
||||
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
db: {
|
||||
schema: 'custom'
|
||||
},
|
||||
auth: {
|
||||
persistSession: true
|
||||
}
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Read more about the [constructor options](/docs/reference/javascript/release-notes#explicit-constructor-options).
|
||||
|
||||
### Auth methods
|
||||
|
||||
The signIn() method has been deprecated in favor of more explicit method signatures to help with type hinting. Previously it was difficult for developers to know what they were missing (e.g., a lot of developers didn't realize they could use passwordless magic links).
|
||||
|
||||
#### Sign in with email and password
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { user, error } = await supabase.auth.signIn({ email, password })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data: { user }, error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Sign in with magic link
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signIn({ email })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signInWithOtp({ email })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Sign in with a third-party provider
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signIn({ provider })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signInWithOAuth({ provider })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Sign in with phone
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signIn({ phone, password })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.signInWithPassword({ phone, password })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Sign in with phone using OTP
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { error } = await supabase.auth.api.sendMobileOTP(phone)
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.signInWithOtp({ phone })
|
||||
|
||||
// After receiving a SMS with a OTP.
|
||||
const { data, error } = await supabase.auth.verifyOtp({ phone, token })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Reset password for email
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.api.resetPasswordForEmail(email)
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.resetPasswordForEmail(email)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Get the user's current session
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const session = supabase.auth.session()
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Get the logged-in user
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const user = supabase.auth.user()
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data: { session } } = await supabase.auth.getSession()
|
||||
const { user } = session
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Update user data for a logged-in user
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { user, error } = await supabase.auth.update({ attributes })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data: { user }, error } = await supabase.auth.updateUser({ attributes })
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Use a custom `access_token` JWT with Supabase
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { user, error } = supabase.auth.setAuth(access_token)
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Data methods
|
||||
|
||||
`.insert()` / `.upsert()` / `.update()` / `.delete()` don't return rows by default: [PR](https://github.com/supabase/postgrest-js/pull/276).
|
||||
|
||||
Previously, these methods return inserted/updated/deleted rows by default (which caused [some confusion](https://github.com/supabase/supabase/discussions/1548)), and you can opt to not return it by specifying `returning: 'minimal'`. Now the default behavior is to not return rows. To return inserted/updated/deleted rows, add a `.select()` call at the end.
|
||||
|
||||
#### Insert and return data
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.insert({ new_data })
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.insert({ new_data }).select()
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Update and return data
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.update({ new_data }).eq('id', id)
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const { data, error } = await supabase.auth.update({ new_data }).eq('id', id).select()
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Realtime methods
|
||||
|
||||
#### Subscribe
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
const userListener = supabase.from('users')
|
||||
.on('*',
|
||||
(payload) => handleAllEventsPayload(payload.new)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
const userListener = supabase.channel('public:user')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'user' },
|
||||
(payload) => handleAllEventsPayload()
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
#### Unsubscribe
|
||||
|
||||
<Tabs
|
||||
groupId="version"
|
||||
values={[
|
||||
{label: 'Before', value: '1.x'},
|
||||
{label: 'After', value: '2.x'},
|
||||
]}>
|
||||
|
||||
<TabItem value="1.x">
|
||||
|
||||
```ts
|
||||
userListener.unsubscribe()
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="2.x">
|
||||
|
||||
```ts
|
||||
supabase.removeChannel(userListener)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -10,7 +10,7 @@ pagination_next: null
|
||||
import ThemedImage from '@theme/ThemedImage'
|
||||
import AngularLogo from '@site/static/img/libraries/angular-icon.svg'
|
||||
import ExpoLogo from '@site/static/img/libraries/expo-icon.svg'
|
||||
import DartLogo from '@site/static/img/libraries/dart-icon.svg'
|
||||
import FlutterLogo from '@site/static/img/libraries/flutter-icon.svg'
|
||||
import JavascriptLogo from '@site/static/img/libraries/javascript-icon.svg'
|
||||
import NextjsDarkLogo from '@site/static/img/libraries/nextjs-dark-icon.svg'
|
||||
import NextjsLightLogo from '@site/static/img/libraries/nextjs-light-icon.svg'
|
||||
@@ -33,13 +33,13 @@ const frameworks = [
|
||||
{
|
||||
name: 'Expo',
|
||||
logo: ExpoLogo,
|
||||
href: 'https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/expo-todo-list',
|
||||
href: '/guides/with-expo',
|
||||
},
|
||||
{ name: 'Flutter', logo: DartLogo, href: '/guides/with-flutter' },
|
||||
{ name: 'Flutter', logo: FlutterLogo, href: '/guides/with-flutter' },
|
||||
{
|
||||
name: 'JavaScript',
|
||||
logo: JavascriptLogo,
|
||||
href: 'https://github.com/supabase/examples/tree/main/supabase-js-v1/auth/javascript-auth',
|
||||
href: '/reference/javascript/installing#javascript',
|
||||
},
|
||||
{
|
||||
name: 'Next.js',
|
||||
|
||||
@@ -2,38 +2,155 @@
|
||||
id: architecture
|
||||
title: Architecture
|
||||
description: 'Supabase design and architecture'
|
||||
# hide_table_of_contents: true
|
||||
---
|
||||
|
||||
Supabase is open source. Wherever possible, we use and support existing tools rather than developing from scratch.
|
||||
We choose open source tools which are scalable and we make them simple to use.
|
||||
Supabase is open source. We choose open source tools which are scalable and make them simple to use.
|
||||
|
||||
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way:
|
||||
our technological choices are quite different; everything we use is open source; and wherever possible, we use and support existing tools rather than developing from scratch.
|
||||
|
||||
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the functionality required to compete with Firebase,
|
||||
while maintaining the scalability required to go beyond it.
|
||||
|
||||
## Architecture
|
||||
|
||||
Each Supabase project consists of several tools:
|
||||
|
||||

|
||||
|
||||
Supabase is not a 1-to-1 mapping of Firebase. While we are building many of the features that Firebase offers, we are not going about it the same way.
|
||||
### PostgreSQL (Database)
|
||||
|
||||
Our technological choices are quite different from Firebase. Everything we use is open source. Wherever possible, we use and support existing tools rather than developing from scratch.
|
||||
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.
|
||||
|
||||
Most notably, we use Postgres rather than a NoSQL store. This choice was deliberate. We believe that no other database offers the scalability and functionality required to compete with 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)
|
||||
- License: [PostgreSQL Licence](https://www.postgresql.org/about/licence/)
|
||||
- Language: C
|
||||
|
||||
## Feature Status
|
||||
### Studio (Dashboard)
|
||||
|
||||
| Product | Feature | Stage | Docs |
|
||||
| -------------------------- | ---------------------- | ----- | ------------------------------------------------ |
|
||||
| Database | Postgres | GA | [Link](/docs/guides/database) |
|
||||
| Database | Webhooks | Alpha | |
|
||||
| Database | Point in time Recovery | Alpha | |
|
||||
| Realtime | Postgres CDC | Beta | [Link](/docs/guides/realtime/postgres-cdc) |
|
||||
| Realtime | Broadcast | Alpha | [Link](/docs/guides/realtime#broadcast) |
|
||||
| Realtime | Presence | Alpha | [Link](/docs/guides/realtime#presence) |
|
||||
| Storage | | Beta | [Link](/docs/guides/storage) |
|
||||
| Storage | CDN | Beta | [Link](/docs/guides/storage-cdn) |
|
||||
| Edge Functions | | Beta | [Link](/docs/guides/functions) |
|
||||
| Auth | OAuth Providers | Beta | [Link](/docs/guides/auth/auth-apple) |
|
||||
| Auth | Passwordless | Beta | [Link](/guides/auth/auth-magic-link) |
|
||||
| Auth | Next.js Auth Helpers | Alpha | [Link](/docs/guides/auth/auth-helpers/nextjs) |
|
||||
| Auth | SvelteKit Auth Helpers | Alpha | [Link](/docs/guides/auth/auth-helpers/sveltekit) |
|
||||
| Public API | | Beta | [Link](/docs/reference/api) |
|
||||
| CLI | | Beta | [Link](/docs/guides/cli) |
|
||||
| Client Library: JavaScript | | GA | [Link](/docs/reference/javascript) |
|
||||
| Client Library: Dart | | Beta | [Link](/docs/reference/dart) |
|
||||
An open source Dashboard for managing your database and services.
|
||||
|
||||
- Official Docs: [Supabase docs](/docs)
|
||||
- Source code: [github.com/supabase/supabase](https://github.com/supabase/supabase/tree/master/studio)
|
||||
- License: [Apache 2](https://github.com/supabase/supabase/blob/master/LICENSE)
|
||||
- Language: TypeScript
|
||||
|
||||
### GoTrue (Auth)
|
||||
|
||||
A JWT-based API for managing users and issuing access tokens. This integrates with PostgreSQL's Row Level Security and the API servers.
|
||||
|
||||
- Official Docs: [Supabase Auth reference docs](/docs/reference/auth)
|
||||
- Source code: [github.com/supabase/gotrue](https://github.com/supabase/gotrue)
|
||||
- License: [MIT](https://github.com/supabase/gotrue/blob/master/LICENSE)
|
||||
- Language: Go
|
||||
|
||||
### 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.
|
||||
|
||||
- Official Docs: [postgrest.org](https://postgrest.org/)
|
||||
- Source code: [github.com/PostgREST/postgrest](https://github.com/PostgREST/postgrest)
|
||||
- License: [MIT](https://github.com/PostgREST/postgrest/blob/main/LICENSE)
|
||||
- Language: Haskel
|
||||
|
||||
### Realtime (API & multiplayer)
|
||||
|
||||
A scalable websocket engine for managing user Presence, broadcasting messages, and streaming database changes.
|
||||
|
||||
- Official Docs: [Supabase Realtime docs](/docs/guides/realtime)
|
||||
- Source code: [github.com/supabase/realtime](https://github.com/supabase/realtime)
|
||||
- License: [Apache 2](https://github.com/supabase/realtime/blob/master/LICENSE)
|
||||
- Language: Elixir
|
||||
|
||||
### Storage API (large file storage)
|
||||
|
||||
An S3-compatible object storage service that stores metadata in Postgres.
|
||||
|
||||
- Official Docs: [Supabase Storage reference docs](/docs/reference/storage)
|
||||
- Source code: [github.com/supabase/storage-api](https://github.com/supabase/storage-api)
|
||||
- License: [Apache 2.0](https://github.com/supabase/storage-api/blob/master/LICENSE)
|
||||
- Language: NodeJS / TypeScript
|
||||
|
||||
### Deno (Edge Functions)
|
||||
|
||||
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)
|
||||
- License: [MIT](https://github.com/denoland/deno/blob/main/LICENSE.md)
|
||||
- Language: TypeScript / Rust
|
||||
|
||||
### postgres-meta (Database management)
|
||||
|
||||
A RESTful API for managing your Postgres. Fetch tables, add roles, and run queries.
|
||||
|
||||
- Official Docs: [supabase.github.io/postgres-meta](https://supabase.github.io/postgres-meta/)
|
||||
- Source code: [github.com/supabase/postgres-meta](https://github.com/supabase/postgres-meta)
|
||||
- License: [Apache 2.0](https://github.com/supabase/postgres-meta/blob/master/LICENSE)
|
||||
- Language: NodeJS / TypeScript
|
||||
|
||||
### PgBouncer
|
||||
|
||||
A lightweight connection pooler for PostgreSQL. This is useful for connecting to Postgres when using Serverless functions.
|
||||
|
||||
- Official Docs: [pgbouncer.org](http://www.pgbouncer.org/)
|
||||
- Source code: [pgbouncer/pgbouncer](https://github.com/pgbouncer/pgbouncer)
|
||||
- License: [ISC](https://github.com/pgbouncer/pgbouncer/blob/master/COPYRIGHT)
|
||||
- Language: C
|
||||
|
||||
### Kong (API Gateway)
|
||||
|
||||
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)
|
||||
- License: [Apache 2.0](https://github.com/Kong/kong/blob/master/LICENSE)
|
||||
- Language: Lua
|
||||
|
||||
## Product Principles
|
||||
|
||||
It is our goal to provide an architecture that any large-scale company would design for themselves,
|
||||
and then provide tooling around that architecture that is easy-to-use for indie-developers and small teams.
|
||||
|
||||
We use a series of principles to ensure that scalability and usability are never mutually exclusive:
|
||||
|
||||
### Everything works in isolation
|
||||
|
||||
Each system must work as a standalone tool with as few moving parts as possible.
|
||||
The litmus test for this is: "Can a user run this product with nothing but a Postgres database?"
|
||||
|
||||
### Everything is integrated
|
||||
|
||||
Supabase is composable. Even though every product works in isolation, each product on the platform needs to 10x the other products.
|
||||
For integration, each tool should expose an API and Webhooks.
|
||||
|
||||
### Everything is extensible
|
||||
|
||||
We're deliberate about adding a new tool, and prefer instead to extend an existing one.
|
||||
This is the opposite of many cloud providers whose product offering expands into niche use-cases. We provide _primitives_ for developers, which allow them to achieve any goal.
|
||||
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 an 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
|
||||
|
||||
We sacrifice short-term wins for long-term gains. For example, it is tempting to run a fork of Postgres with additional functionality which only our customers need.
|
||||
Instead, we prefer to support efforts to upstream missing functionality so that the entire community benefits. This has the additional benefit of ensuring portability and longevity.
|
||||
|
||||
### Build for developers
|
||||
|
||||
"Developers" are a specific profile of user: they are _builders_.
|
||||
When assessing impact as a function of effort, developers have a large efficiency due to the type of products and systems they can build.
|
||||
As the profile of a developer changes over time, Supabase will continue to evolve the product to fit this evolving profile.
|
||||
|
||||
### Support existing tools
|
||||
|
||||
Supabase supports existing tools and communities wherever possible. Supabase is more like a "community of communities" - each tool typically has its own community
|
||||
which we work with.
|
||||
Open source is something we approach [collaboratively](https://supabase.com/blog/supabase-series-b#giving-back): we employ maintainers, sponsor projects, invest in businesses, and develop our own open source tools.
|
||||
@@ -33,6 +33,6 @@ We only support PostgreSQL. It's unlikely we'll ever move away from Postgres; ho
|
||||
|
||||
### Do you have a library for `[some other language]`?
|
||||
|
||||
We officially support [JavaScript](/docs/reference/javascript/installing) and [Dart](/docs/reference/dart/installing).
|
||||
We officially support [JavaScript](/docs/reference/javascript/installing) and [Flutter](/docs/reference/dart/installing).
|
||||
|
||||
You can find community-supported libraries in our [GitHub Community](https://github.com/supabase-community), and you can also help us to identify the most popular languages by [voting for a new client library](https://github.com/supabase/supabase/discussions/5).
|
||||
@@ -0,0 +1,66 @@
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import UserManagementSQLTemplate from './user_management_quickstart_sql_template.md'
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
groupId="language"
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
<UserManagementSQLTemplate />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
@@ -0,0 +1,11 @@
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a basic user management app. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
@@ -0,0 +1,55 @@
|
||||
```sql
|
||||
-- Create a table for public profiles
|
||||
create table profiles (
|
||||
id uuid references auth.users not null primary key,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
-- Set up Row Level Security (RLS)
|
||||
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
|
||||
alter table profiles
|
||||
enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone." on profiles
|
||||
for select using (true);
|
||||
|
||||
create policy "Users can insert their own profile." on profiles
|
||||
for insert with check (auth.uid() = id);
|
||||
|
||||
create policy "Users can update own profile." on profiles
|
||||
for update using (auth.uid() = id);
|
||||
|
||||
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
|
||||
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.profiles (id, full_name, avatar_url)
|
||||
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
-- Set up access controls for storage.
|
||||
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
|
||||
create policy "Avatar images are publicly accessible." on storage.objects
|
||||
for select using (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can upload an avatar." on storage.objects
|
||||
for insert with check (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can update their own avatar." on storage.objects
|
||||
for update using (auth.uid() = owner) with check (bucket_id = 'avatars');
|
||||
```
|
||||
@@ -65,6 +65,10 @@ This will allow your users to use your custom Azure login page when logging in.
|
||||
|
||||
## Add login code to your client app
|
||||
|
||||
:::tip
|
||||
Supabase Auth requires that Azure returns a valid email address. Therefore you must request the `email` scope in the `signIn` method above.
|
||||
:::
|
||||
|
||||
When your user signs in, call [signInWithOAuth()](/docs/reference/javascript/auth-signinwithoauth) with `azure` as the `provider`:
|
||||
|
||||
```js
|
||||
|
||||
@@ -12,7 +12,7 @@ import TabItem from '@theme/TabItem'
|
||||
Setting up Email logins for your Supabase application.
|
||||
|
||||
- Add Email authenticator to your [Supabase Project](https://app.supabase.com)
|
||||
- Add the login code to your application - [JavaScript](https://github.com/supabase/supabase-js) | [Dart](https://github.com/supabase/supabase-dart)
|
||||
- Add the login code to your application - [JavaScript](https://github.com/supabase/supabase-js) | [Flutter](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
## Configure email settings
|
||||
|
||||
@@ -55,11 +55,11 @@ async function signInWithEmail() {
|
||||
|
||||
<TabItem value="dart">
|
||||
|
||||
When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address and password:
|
||||
When your user signs in, call [signInWithPassword()](/docs/reference/dart/auth-signinwithpassword) with their email address and password:
|
||||
|
||||
```dart
|
||||
Future<void> signInWithEmail() async {
|
||||
await supabase.auth.signIn(
|
||||
final AuthResponse res = await supabase.auth.signInWithPassword(
|
||||
email: 'example@email.com',
|
||||
password: 'example-password'
|
||||
);
|
||||
@@ -106,4 +106,4 @@ Future<void> signOut() async {
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Dart Client](https://github.com/supabase/supabase-dart)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -290,9 +290,9 @@ export default function ProtectedPage({
|
||||
|
||||
export const getServerSideProps = withPageAuth({
|
||||
redirectTo: '/',
|
||||
async getServerSideProps(ctx, supabaseServerClient) {
|
||||
async getServerSideProps(ctx, supabase) {
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabaseServerClient(ctx).from('test').select('*')
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
return { props: { data } }
|
||||
},
|
||||
})
|
||||
@@ -366,13 +366,9 @@ Wrap an API Route to check that the user has a valid session. If they're not log
|
||||
```js title="pages/api/protected-route.js"
|
||||
import { withApiAuth } from '@supabase/auth-helpers-nextjs'
|
||||
|
||||
export default withApiAuth(async function ProtectedRoute(
|
||||
req,
|
||||
res,
|
||||
supabaseServerClient
|
||||
) {
|
||||
export default withApiAuth(async function ProtectedRoute(req, res, supabase) {
|
||||
// Run queries with RLS on the server
|
||||
const { data } = await supabaseServerClient.from('test').select('*')
|
||||
const { data } = await supabase.from('test').select('*')
|
||||
res.json(data)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -15,7 +15,7 @@ Magic links only work with email addresses. By default, a user can only request
|
||||
Setting up Magic Link logins for your Supabase application.
|
||||
|
||||
- Add Magic Link authenticator to your [Supabase Project](https://app.supabase.com)
|
||||
- Add the login code to your application - [JavaScript](https://github.com/supabase/supabase-js) | [Dart](https://github.com/supabase/supabase-dart)
|
||||
- Add the login code to your application - [JavaScript](https://github.com/supabase/supabase-js) | [Flutter](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
## Add Magic Link into your Supabase Project
|
||||
|
||||
@@ -50,11 +50,11 @@ async function signInWithEmail() {
|
||||
|
||||
<TabItem value="dart">
|
||||
|
||||
When your user signs in, call [signIn()](/docs/reference/dart/auth-signin) with their email address:
|
||||
When your user signs in, call [signIn()](/docs/reference/dart/auth-signinwithotp) with their email address:
|
||||
|
||||
```dart
|
||||
Future<void> signInWithEmail() async {
|
||||
await supabase.auth.signIn(email: 'example@email.com');
|
||||
final AuthResponse res = await supabase.auth.signinwithotp(email: 'example@email.com');
|
||||
}
|
||||
```
|
||||
|
||||
@@ -98,4 +98,4 @@ Future<void> signOut() async {
|
||||
|
||||
- [Supabase Account - Free Tier OK](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Dart Client](https://github.com/supabase/supabase-dart)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -0,0 +1,625 @@
|
||||
---
|
||||
id: auth-mfa
|
||||
title: 'Multi-Factor Authentication'
|
||||
description: Add an additional layer of security to your apps with Supabase Auth multi-factor authentication.
|
||||
---
|
||||
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
|
||||
:::note
|
||||
Multi-Factor Authentication is in early access preview only. Although we
|
||||
believe it is production ready, APIs and some behavior may change based on
|
||||
feedback we receive in the preview period.
|
||||
:::
|
||||
|
||||
Multi-factor authentication (MFA), sometimes called two-factor
|
||||
authentication (2FA), adds an additional layer of security to your
|
||||
application by verifying their identity through
|
||||
additional verification steps.
|
||||
|
||||
It is considered a best practice to use MFA for your applications.
|
||||
|
||||
Users with weak passwords or compromised social login accounts are
|
||||
prone to malicious account takeovers. These can be prevented with
|
||||
MFA because they require the user to provide proof of
|
||||
both of these:
|
||||
|
||||
- Something they know.
|
||||
Password, or access to a social-login account.
|
||||
- Something they have.
|
||||
Access to an authenticator app (a.k.a. TOTP), mobile phone or recovery code.
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase Auth implements only Time-based One Time Factor(TOTP) multi-factor authentication. This type of
|
||||
multi-factor authentication uses a timed one-time password generated from an
|
||||
authenticator app in the control of users.
|
||||
|
||||
Applications using MFA require two important flows:
|
||||
|
||||
1. **Enrollment flow.**
|
||||
This lets users set up and control MFA in your app.
|
||||
2. **Authentication flow.**
|
||||
This lets users sign in using any factors after the conventional login step.
|
||||
|
||||
Supabase Auth provides:
|
||||
- **Enrollment API** - build rich user interfaces for adding and removing factors.
|
||||
- **Challenge and Verify APIs** - securely verify that the user has access to a factor.
|
||||
- **List Factors API** - build rich user interfaces for signing in with additional factors.
|
||||
|
||||
Below is a flow chart illustrating how these APIs work together to enable MFA
|
||||
features in your app.
|
||||
|
||||
<Mermaid chart={`
|
||||
graph TD;
|
||||
InitS((Setup flow)) --> SAAL1[/Session is AAL1/] --> Enroll[Enroll API] --> ShowQR[Show QR code] --> Scan([User: Scan QR code in authenticator]) --> Enter([User: Enter code]) --> Verify[Challenge + Verify API] --> Check{{Is code correct?}}
|
||||
Check -->|Yes| AAL2[/Upgrade to AAL2/] --> Done((Done))
|
||||
Check -->|No| Enter
|
||||
InitA((Login flow)) --> SignIn([User: Sign-in]) --> AAL1[/Upgrade to AAL1/] --> ListFactors[List Factors API]
|
||||
ListFactors -->|1 or more factors| OpenAuth([User: Open authenticator]) --> Enter
|
||||
ListFactors -->|0 factors| Setup[[Setup flow]]
|
||||
`} />
|
||||
|
||||
These sets of APIs let you control the MFA experience that works for you. You
|
||||
can create flows where MFA is optional, mandatory for all or only specific
|
||||
groups of users.
|
||||
|
||||
Once users have enrolled or signed-in with a factor, Supabase Auth adds
|
||||
additional metadata to the user's access token (JWT) that your application can
|
||||
use to allow or deny access.
|
||||
|
||||
This information is represented by an [Authenticator Assurance
|
||||
Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), a
|
||||
standard measure about the assurance Supabase Auth has of the user's identity
|
||||
for that particular session. There are two levels recognized today:
|
||||
|
||||
1. **Assurance Level 1: `aal1`**
|
||||
Means that the user's identity was verified using a conventional login method
|
||||
such as email+password, magic link, one-time password, phone auth or social
|
||||
login.
|
||||
2. **Assurance Level 2: `aal2`**
|
||||
Means that the user's identity was additionally verified using at least one
|
||||
second factor, such as a TOTP code.
|
||||
|
||||
This assurance level is encoded in the `aal` claim in the JWT associated with
|
||||
the user. By decoding this value you can create custom authorization rules in
|
||||
your frontend, backend and database that will enforce the MFA policy that works
|
||||
for your application. JWTs without an `aal` claim are at the `aal1` level.
|
||||
|
||||
## Adding to your app
|
||||
|
||||
Adding MFA to your app involves these three steps:
|
||||
|
||||
1. **Add enrollment flow.**
|
||||
You need to provide a UI within your app that your users will be able to set-up
|
||||
MFA in. You can add this right after sign-up, or as part of a separate flow in
|
||||
the settings portion of your app.
|
||||
2. **Add challenge step to login.**
|
||||
If a user has set-up MFA, your app's login flow needs to present a challenge
|
||||
screen to the user asking them to prove they have access to the additional
|
||||
factor.
|
||||
3. **Enforce rules for MFA logins.**
|
||||
Once your users have a way to enroll and log in with MFA, you need to enforce
|
||||
authorization rules across your app: on the frontend, backend, API servers or
|
||||
Row-Level Security policies.
|
||||
|
||||
### Add enrollment flow
|
||||
|
||||
An enrollment flow provides a UI for users to set up additional authentication factors.
|
||||
Most applications add the enrollment flow in two places within their app:
|
||||
|
||||
1. Right after login or sign up.
|
||||
This lets users quickly set up MFA immediately after they log in or create an
|
||||
account. We recommend encouraging all users to set up MFA if that makes sense
|
||||
for your application. Many applications offer this as an opt-in step in an
|
||||
effort to reduce onboarding friction.
|
||||
2. From within a settings page.
|
||||
Allows users to set up, disable or modify their MFA settings.
|
||||
|
||||
We recommend building one generic flow that you can reuse in both cases with
|
||||
minor modifications.
|
||||
|
||||
Enrolling a factor for use with MFA takes three steps:
|
||||
|
||||
1. Call `supabase.auth.mfa.enroll()`.
|
||||
This method returns a QR code and a secret. Display the QR
|
||||
code to the user and ask them to scan it with their authenticator application.
|
||||
If they are unable to scan the QR code, show the secret in plain text which
|
||||
they can type or paste into their authenticator app.
|
||||
2. Calling the `supabase.auth.mfa.challenge()` API.
|
||||
This prepares Supabase Auth to accept a verification code from the user
|
||||
and returns a challenge ID.
|
||||
3. Calling the `supabase.auth.mfa.verify()` API.
|
||||
This verifies that the user has indeed added the secret from step (1) into
|
||||
their app and is working correctly. If the verification succeeds, the factor
|
||||
immediately becomes active for the user account. If not, you should repeat
|
||||
steps 2 and 3.
|
||||
|
||||
#### Example: React
|
||||
|
||||
Below is an example that creates a new `EnrollMFA` component that illustrates
|
||||
the important pieces of the MFA enrollment flow.
|
||||
|
||||
- When the component appears on screen, the `supabase.auth.mfa.enroll()` API is
|
||||
called once to start the process of enrolling a new factor for the current
|
||||
user.
|
||||
- This API returns a QR code in the SVG format, which is shown on screen using
|
||||
a normal `<img>` tag by encoding the SVG as a data URL.
|
||||
- Once the user has scanned the QR code with their authenticator app, they
|
||||
should enter the verification code within the `verifyCode` input field and
|
||||
click on `Enable`.
|
||||
- A challenge is created using the `supabase.auth.mfa.challenge()` API and the
|
||||
code from the user is submitted for verification using the
|
||||
`supabase.auth.mfa.verify()` challenge.
|
||||
- `onEnabled` is a callback that notifies the other components that enrollment
|
||||
has completed.
|
||||
- `onCancelled` is a callback that notifies the other components that the user
|
||||
has clicked the `Cancel` button.
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* EnrollMFA shows a simple enrollment dialog. When shown on screen it calls
|
||||
* the `enroll` API. Each time a user clicks the Enable button it calls the
|
||||
* `challenge` and `verify` APIs to check if the code provided by the user is
|
||||
* valid.
|
||||
* When enrollment is successful, it calls `onEnrolled`. When the user clicks
|
||||
* Cancel the `onCancelled` callback is called.
|
||||
*/
|
||||
export function EnrollMFA({
|
||||
onEnrolled,
|
||||
onCancelled,
|
||||
}: {
|
||||
onEnrolled: () => void;
|
||||
onCancelled: () => void;
|
||||
}) {
|
||||
const [factorId, setFactorId] = useState("");
|
||||
const [qr, setQR] = useState(""); // holds the QR code image SVG
|
||||
const [verifyCode, setVerifyCode] = useState(""); // contains the code entered by the user
|
||||
const [error, setError] = useState(""); // holds an error message
|
||||
|
||||
const onEnableClicked = () => {
|
||||
setError("");
|
||||
|
||||
(async () => {
|
||||
const challenge = await supabase.auth.mfa.challenge({ factorId });
|
||||
if (challenge.error) {
|
||||
setError(challenge.error.message);
|
||||
throw challenge.error;
|
||||
}
|
||||
|
||||
const challengeId = challenge.data.id;
|
||||
|
||||
const verify = await supabase.auth.mfa.verify({
|
||||
factorId,
|
||||
challengeId,
|
||||
code: verifyCode,
|
||||
});
|
||||
if (verify.error) {
|
||||
setError(verify.error.message);
|
||||
throw verify.error;
|
||||
}
|
||||
|
||||
onEnrolled();
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data, error } = await supabase.auth.mfa.enroll({
|
||||
factorType: "totp",
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setFactorId(data.id);
|
||||
|
||||
// Supabase Auth returns an SVG QR code which you can convert into a data
|
||||
// URL that you can place in an <img> tag.
|
||||
setQR(data.totp.qr_code);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <div className="error">{error}</div>}
|
||||
<img src={qr} />
|
||||
<input
|
||||
type="text"
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value.trim())}
|
||||
/>
|
||||
<input type="button" value="Enable" onClick={onEnableClicked} />
|
||||
<input type="button" value="Cancel" onClick={onCancelled} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Add challenge step to login
|
||||
|
||||
Once a user has logged in via their first factor (email+password, magic link,
|
||||
one time password, social login...) you need to perform a check if any
|
||||
additional factors need to be verified.
|
||||
|
||||
This can be done by using the
|
||||
`supabase.auth.mfa.getAuthenticatorAssuranceLevel()` API. When the user signs
|
||||
in and is redirected back to your app, you should call this method to extract
|
||||
the user's current and next authenticator assurance level (AAL).
|
||||
|
||||
Therefore if you receive a `currentLevel` which is `aal1` but a `nextLevel`
|
||||
of `aal2`, the user should be given the option to go through MFA.
|
||||
|
||||
Below is a table that explains the combined meaning.
|
||||
|
||||
| Current Level | Next Level | Meaning |
|
||||
| -------------:|:---------- |:------------------------------------------ |
|
||||
| `aal1` | `aal1` | User does not have MFA enrolled. |
|
||||
| `aal1` | `aal2` | User has an MFA factor enrolled but has not verified it. |
|
||||
| `aal2` | `aal2` | User has verified their MFA factor. |
|
||||
| `aal2` | `aal1` | User has disabled their MFA factor. (Stale JWT.) |
|
||||
|
||||
#### Example: React
|
||||
|
||||
Adding the challenge step to login depends heavily on the architecture of your
|
||||
app. However, a fairly common way to structure React apps is to have a large
|
||||
component (often named `App`) which contains most of the authenticated
|
||||
application logic.
|
||||
|
||||
This example will wrap this component with logic that will show an MFA
|
||||
challenge screen if necessary, before showing the full application. This is
|
||||
illustrated in the `AppWithMFA` example below.
|
||||
|
||||
```tsx
|
||||
function AppWithMFA() {
|
||||
const [readyToShow, setReadyToShow] = useState(false);
|
||||
const [showMFAScreen, setShowMFAScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { data, error } =
|
||||
await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (data.nextLevel === "aal2" && data.nextLevel !== data.currentLevel) {
|
||||
setShowMFAScreen(true);
|
||||
}
|
||||
} finally {
|
||||
setReadyToShow(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (readyToShow) {
|
||||
if (showMFAScreen) {
|
||||
return <AuthMFA />;
|
||||
}
|
||||
|
||||
return <App />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
```
|
||||
|
||||
- `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` does return a promise.
|
||||
Don't worry, this is a very fast method (microseconds) as it rarely uses the
|
||||
network.
|
||||
- `readyToShow` only makes sure the AAL check completes before showing any
|
||||
application UI to the user.
|
||||
- If the current level can be upgraded to the next one, the MFA screen is
|
||||
shown.
|
||||
- Once the challenge is successful, the `App` component is finally rendered on
|
||||
screen.
|
||||
|
||||
Below is the component that implements the challenge and verify logic.
|
||||
|
||||
```tsx
|
||||
function AuthMFA() {
|
||||
const [verifyCode, setVerifyCode] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const onSubmitClicked = () => {
|
||||
setError("");
|
||||
|
||||
(async () => {
|
||||
const factors = await supabase.auth.mfa.listFactors();
|
||||
if (factors.error) {
|
||||
throw factors.error;
|
||||
}
|
||||
|
||||
const totpFactor = factors.data.totp[0];
|
||||
|
||||
if (!totpFactor) {
|
||||
throw new Error("No TOTP factors found!");
|
||||
}
|
||||
|
||||
const factorId = totpFactor.id;
|
||||
|
||||
const challenge = await supabase.auth.mfa.challenge({ factorId });
|
||||
if (challenge.error) {
|
||||
setError(challenge.error.message);
|
||||
throw challenge.error;
|
||||
}
|
||||
|
||||
const challengeId = challenge.data.id;
|
||||
|
||||
const verify = await supabase.auth.mfa.verify({
|
||||
factorId,
|
||||
challengeId,
|
||||
code: verifyCode,
|
||||
});
|
||||
if (verify.error) {
|
||||
setError(verify.error.message);
|
||||
throw verify.error;
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Please enter the code from your authenticator app.</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
<input
|
||||
type="text"
|
||||
value={verifyCode}
|
||||
onChange={(e) => setVerifyCode(e.target.value.trim())}
|
||||
/>
|
||||
<input type="button" value="Submit" onClick={onSubmitClicked} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- You can extract the available MFA factors for the user by calling
|
||||
`supabase.auth.mfa.listFactors()`. Don't worry this method is also very quick
|
||||
and rarely uses the network.
|
||||
- If `listFactors()` returns more than one factor (or of a different type) you
|
||||
should present the user with a choice. For simplicity this is not shown in
|
||||
the example.
|
||||
- Each time the user presses the "Submit" button a new challenge is created for
|
||||
the chosen factor (in this case the first one) and it is immediately
|
||||
verified. Any errors are displayed to the user.
|
||||
- On successful verification, the client library will refresh the session in
|
||||
the background automatically and finally call the `onSuccess` callback, which
|
||||
will show the authenticated `App` component on screen.
|
||||
|
||||
### Enforce rules for MFA logins
|
||||
|
||||
Adding MFA to your app's UI does not in-and-of-itself offer a higher level of
|
||||
security to your users. You also need to enforce the MFA rules in your
|
||||
application's database, APIs and server-side rendering.
|
||||
|
||||
Depending on your application's needs, there are three ways you can choose to
|
||||
enforce MFA.
|
||||
|
||||
1. **Enforce for all users (new and existing).**
|
||||
Any user account will have to enroll MFA to continue using your app.
|
||||
The application will not allow access without going through MFA first.
|
||||
2. **Enforce for new users only.**
|
||||
Only new users will be forced to enroll MFA, while old users will be encouraged
|
||||
to do so.
|
||||
The application will not allow access for new users without going through MFA
|
||||
first.
|
||||
3. **Enforce only for users that have opted-in.**
|
||||
Users that want MFA can enroll in it and the application will not allow access
|
||||
without going through MFA first.
|
||||
|
||||
#### Database
|
||||
|
||||
Your app should sufficiently deny or allow access to tables or rows based on
|
||||
the user's current and possible authenticator levels.
|
||||
|
||||
:::warning
|
||||
PostgreSQL has two types of policies: permissive and restrictive. This guide
|
||||
uses restrictive policies. Make sure you don't omit the `as restrictive`
|
||||
clause.
|
||||
:::
|
||||
|
||||
##### Enforce for all users (new and existing)
|
||||
|
||||
If your app falls under this case, this is a template Row Level Security policy
|
||||
you can apply to all your tables:
|
||||
|
||||
```sql
|
||||
create policy "Policy name."
|
||||
on table_name
|
||||
as restrictive
|
||||
to authenticated
|
||||
using (auth.jwt()->>'aal' = 'aal2');
|
||||
```
|
||||
|
||||
- Here the policy will not accept any JWTs with an `aal` claim other than
|
||||
`aal2`, which is the highest authenticator assurance level.
|
||||
- **Using `as restrictive` ensures this policy will restrict all commands on the
|
||||
table regardless of other policies!**
|
||||
|
||||
##### Enforce for new users only
|
||||
|
||||
If your app falls under this case, the rules get more complex. User accounts
|
||||
created past a certain timestamp must have a `aal2` level to access the
|
||||
database.
|
||||
|
||||
```sql
|
||||
create policy "Policy name."
|
||||
on table_name
|
||||
as restrictive -- very important!
|
||||
to authenticated
|
||||
using
|
||||
(array[auth.jwt()->>'aal'] <@ (
|
||||
select
|
||||
case
|
||||
when created_at >= '2022-12-12T00:00:00Z' then array['aal2']
|
||||
else array['aal1', 'aal2', NULL]
|
||||
end as aal
|
||||
from auth.users
|
||||
where auth.uid() = id));
|
||||
```
|
||||
|
||||
- The policy will accept both `aal1` and `aal2` for users with a `created_at`
|
||||
timestamp prior to 12th December 2022 at 00:00 UTC, but will only accept
|
||||
`aal2` for all other timestamps.
|
||||
- The `<@` operator is PostgreSQL's ["contained in"
|
||||
operator.](https://www.postgresql.org/docs/current/functions-array.html)
|
||||
- `NULL` appears because some JWTs originating from prior to the introduction
|
||||
of MFA in Supabase Auth will not contain an `aal` claim.
|
||||
- **Using `as restrictive` ensures this policy will restrict all commands on the
|
||||
table regardless of other policies!**
|
||||
|
||||
##### Enforce only for users that have opted-in
|
||||
|
||||
Users that have enrolled MFA on their account are expecting that your
|
||||
application only works for them if they've gone through MFA.
|
||||
|
||||
```sql
|
||||
create policy "Policy name."
|
||||
on table_name
|
||||
as restrictive -- very important!
|
||||
to authenticated
|
||||
using (
|
||||
array[auth.jwt()->>'aal'] <@ (
|
||||
select
|
||||
case
|
||||
when count(id) > 0 then array['aal2']
|
||||
else array['aal1', 'aal2', NULL]
|
||||
end as aal
|
||||
from auth.mfa_factors
|
||||
where auth.uid() = user_id and status = 'verified'
|
||||
));
|
||||
```
|
||||
|
||||
- The policy will only accept only `aal2` when the user has at least one MFA
|
||||
factor verified.
|
||||
- Otherwise, it will accept both `aal1` and `aal2`.
|
||||
- The `<@` operator is PostgreSQL's ["contained in"
|
||||
operator.](https://www.postgresql.org/docs/current/functions-array.html)
|
||||
- `NULL` appears because some JWTs originating from prior to the introduction
|
||||
of MFA in Supabase Auth will not contain an `aal` claim.
|
||||
- **Using `as restrictive` ensures this policy will restrict all commands on the
|
||||
table regardless of other policies!**
|
||||
|
||||
### Server-Side Rendering
|
||||
|
||||
:::tip
|
||||
When using the Supabase JavaScript library in a server-side rendering context,
|
||||
make sure you always create a new object for each request! This will prevent
|
||||
you from accidentally rendering and serving content belonging to different
|
||||
users.
|
||||
:::
|
||||
|
||||
It is possible to enforce MFA on the Server-Side Rendering level. However, this
|
||||
can be tricky do to well.
|
||||
|
||||
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and
|
||||
`supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session
|
||||
and any factors that are enabled for a user, similar to how you would use these
|
||||
on the browser.
|
||||
|
||||
However, encountering a different AAL level on the server may not actually be a
|
||||
security problem. Consider these likely scenarios:
|
||||
|
||||
1. User signed-in with a conventional method but closed their tab on the MFA
|
||||
flow.
|
||||
2. User forgot a tab open for a very long time. (This happens more often than
|
||||
you might imagine.)
|
||||
3. User has lost their authenticator device and is confused about the next
|
||||
steps.
|
||||
|
||||
We thus recommend you redirect users to a page where they can authenticate
|
||||
using their additional factor, instead of rendering a HTTP 401 Unauthorized or
|
||||
HTTP 403 Forbidden content.
|
||||
|
||||
### APIs
|
||||
|
||||
If your application uses the Supabase Database, Storage or Edge Functions, just
|
||||
using Row Level Security policies will give you sufficient protection. In the
|
||||
event that you have other APIs that you wish to protect, follow these general
|
||||
guidelines:
|
||||
|
||||
1. **Use a good JWT verification and parsing library for your language.**
|
||||
This will let you securely parse JWTs and extract their claims.
|
||||
2. **Retrieve the `aal` claim from the JWT and compare its value according to
|
||||
your needs.**
|
||||
If you've encountered an AAL level that can be increased, ask the user to
|
||||
continue the login process instead of logging them out.
|
||||
3. **Use the `https://<project-ref>.supabase.co/rest/v1/auth/factors` REST
|
||||
endpoint to identify if the user has enrolled any MFA factors.**
|
||||
Only `verified` factors should be acted upon.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### Why is there a challenge and verify API when challenge does not do much?
|
||||
|
||||
TOTP is not going to be the only MFA factor Supabase Auth is going to support
|
||||
in the future. By separating out the challenge and verify steps, we're making
|
||||
the library forward compatible with new factors we may add in the future --
|
||||
such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would
|
||||
actually send out the SMS with the authentication code.
|
||||
|
||||
### What's inside the QR code?
|
||||
|
||||
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially
|
||||
introduced by Google
|
||||
Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
||||
but is now universally accepted by all authenticator apps.
|
||||
|
||||
### How do I check _when_ a user went through MFA?
|
||||
|
||||
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods
|
||||
Reference) claim. It is an array of objects that indicate what authentication
|
||||
methods the user has used so far.
|
||||
|
||||
For example, the following structure describes a user that first signed in with
|
||||
a password-based method, and then went through TOTP MFA 2 minutes and 12
|
||||
seconds later. The entries are ordered most recent method first!
|
||||
|
||||
```json
|
||||
{
|
||||
"amr": [
|
||||
{
|
||||
"method": "mfa/totp",
|
||||
"timestamp": 1666086056
|
||||
},
|
||||
{
|
||||
"method": "password",
|
||||
"timestamp": 1666085924
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy
|
||||
access to this information in your browser app.
|
||||
|
||||
You can use this PostgreSQL snippet in RLS policies, too:
|
||||
|
||||
```sql
|
||||
json_query_path(auth.jwt(), '$.amr[0]')
|
||||
```
|
||||
|
||||
- [`json_query_path(json,
|
||||
path)`](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-PROCESSING-TABLE)
|
||||
is a function that allows access to elements in a JSON object according to a
|
||||
[SQL/JSON
|
||||
path](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-PATH).
|
||||
- `$.amr[0]` is a SQL/JSON path expression that fetches the most recent
|
||||
authentication method in the JWT.
|
||||
|
||||
Once you have extracted the most recent entry in the array, you can compare the
|
||||
`method` and `timestamp` to enforce stricter rules.
|
||||
|
||||
Currently recognized methods are:
|
||||
|
||||
- `password` - any password based sign in.
|
||||
- `otp` - any one-time password based sign in (email code, SMS code, magic
|
||||
link).
|
||||
- `oauth` - any OAuth based sign in (social login).
|
||||
- `mfa/totp` - a TOTP additional factor.
|
||||
|
||||
This list will expand in the future.
|
||||
|
||||
@@ -145,6 +145,7 @@ values={[
|
||||
let { session, error } = await supabase.auth.verifyOTP({
|
||||
phone: '+13334445555',
|
||||
token: '123456',
|
||||
type: 'sms',
|
||||
})
|
||||
```
|
||||
|
||||
@@ -260,6 +261,7 @@ values={[
|
||||
let { session, error } = await supabase.auth.verifyOTP({
|
||||
phone: '+13334445555',
|
||||
token: '123456',
|
||||
type: 'sms',
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
id: server-side-rendering
|
||||
title: Server-Side Rendering
|
||||
description: Render pages with user information on the server.
|
||||
---
|
||||
|
||||
Single-page apps with server-side rendering (SSR) is a popular way to optimize rendering
|
||||
performance and leverage advanced caching strategies.
|
||||
|
||||
Supabase Auth supports server-side rendering when you need access to user
|
||||
information, or your server needs to authorize API requests on behalf of your
|
||||
user to render content.
|
||||
|
||||
When a user authenticates with Supabase Auth, two pieces of information are
|
||||
issued by the server:
|
||||
|
||||
1. **Access token** in the form of a JWT.
|
||||
2. **Refresh token** which is a randomly generated string.
|
||||
|
||||
Most Supabase projects have their auth server listening on
|
||||
`<project-ref>.supabase.co/auth/v1`, thus the access token and refresh token are
|
||||
set as `sb-access-token` and `sb-refresh-token` cookies on the
|
||||
`<project-ref>.supabase.co` domain.
|
||||
|
||||
:::note
|
||||
These cookie names are for internal Supabase use only and may change without
|
||||
warning. They are included in this guide for illustration purposes only.
|
||||
:::
|
||||
|
||||
Web browsers limit access to cookies across domains, consistent with the
|
||||
[Same-Origin Policy
|
||||
(SOP)](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy).
|
||||
|
||||
Your web application cannot access these cookies,
|
||||
nor will these cookies be sent to your application's server.
|
||||
|
||||
## Understanding the authentication flow
|
||||
|
||||
When you call one of the `signIn` methods, the client library running in the
|
||||
browser sends the request to the Supabase Auth server. The Auth server determines
|
||||
whether to verify a phone number, email and password combination, a Magic Link,
|
||||
or use a social login (if you have any setup in your project).
|
||||
|
||||
Upon successful verification of the identity of the user, the Supabase Auth
|
||||
server redirects the user back to your single-page app.
|
||||
|
||||
:::tip
|
||||
You can configure [redirects URLs](https://app.supabase.com/project/_/auth/settings) in the Supabase Dashboard. You can use wildcard match patterns
|
||||
like `*` and `**` to allow redirects to different forms of URLs.
|
||||
:::
|
||||
|
||||
These redirect URLs have the following structure:
|
||||
|
||||
```
|
||||
https://yourapp.com/...#access_token=<...>&refresh_token=<...>&...
|
||||
```
|
||||
|
||||
The first access and refresh tokens after a successful verification are
|
||||
contained in the URL fragment (anything after the `#` sign) of the redirect
|
||||
location. This is intentional and not configurable.
|
||||
|
||||
The client libraries are designed to listen for this type of URL, extract
|
||||
the access token, refresh token and some extra information from it, and finally
|
||||
persist it in local storage for further use by the library and your app.
|
||||
|
||||
:::info
|
||||
Web browsers do not send the URL fragment to the server they're making the
|
||||
request to. Since you may not be hosting the single-page app on a server under
|
||||
your direct control (such as on GitHub Pages or other freemium hosting
|
||||
providers), we want to prevent hosting services from getting access to your
|
||||
user's authorization credentials by default. Even if the server is under your
|
||||
direct control, `GET` requests and their full URLs are often logged. This
|
||||
approach also avoids leaking credentials in request or access logs.
|
||||
:::
|
||||
|
||||
## Bringing it together
|
||||
|
||||
As seen from the authentication flow, the initial request after successful
|
||||
login made by the browser to your app's server after user login **does not
|
||||
contain any information about the user**. This is because first the client-side
|
||||
JavaScript library must run before it makes the access and refresh token
|
||||
available to your server.
|
||||
|
||||
It is very important to make sure that the redirect route right after login
|
||||
works without any server-side rendering. Other routes requiring authorization
|
||||
do not have the same limitation, provided you send the access and refresh
|
||||
tokens to your server.
|
||||
|
||||
This is traditionally done by setting cookies. Here's an example you
|
||||
can add to the root of your application:
|
||||
|
||||
```typescript
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === "SIGNED_OUT" || event === "USER_DELETED") {
|
||||
// delete cookies on sign out
|
||||
const expires = new Date(0).toUTCString();
|
||||
document.cookie = `my-access-token=; path=/; expires=${expires}; SameSite=Lax; secure`;
|
||||
document.cookie = `my-refresh-token=; path=/; expires=${expires}; SameSite=Lax; secure`;
|
||||
} else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
|
||||
const maxAge = 100 * 365 * 24 * 60 * 60; // 100 years, never expires
|
||||
document.cookie = `my-access-token=${session.access_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`;
|
||||
document.cookie = `my-refresh-token=${session.refresh_token}; path=/; max-age=${maxAge}; SameSite=Lax; secure`;
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This uses the standard
|
||||
[`document.cookie` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)
|
||||
to set cookies on all paths of your app's domain. All subsequent requests
|
||||
made by the browser to your app's server include the `my-access-token` and
|
||||
`my-refresh-token` cookies (the names of the cookies and additional
|
||||
parameters can be changed).
|
||||
|
||||
In your server-side rendering code you can now access user and session
|
||||
information:
|
||||
|
||||
|
||||
```typescript
|
||||
const refreshToken = req.cookies['my-refresh-token'];
|
||||
const accessToken = req.cookies['my-access-token'];
|
||||
|
||||
if (refreshToken && accessToken) {
|
||||
await supabase.auth.setSession({
|
||||
refresh_token: refreshToken,
|
||||
access_token: accessToken
|
||||
});
|
||||
} else {
|
||||
// make sure you handle this case!
|
||||
throw new Error("User is not authenticated.")
|
||||
}
|
||||
|
||||
// returns user information
|
||||
await supabase.auth.getUser();
|
||||
```
|
||||
|
||||
Use `setSession({ access_token, refresh_token })` instead of
|
||||
`setSession(refreshToken)` or `getUser(accessToken)` as refresh tokens or access tokens alone do not properly identify a user session.
|
||||
|
||||
Access tokens are valid only for a short amount of time.
|
||||
|
||||
Even though refresh tokens are long-lived, there is no guarantee that a user
|
||||
has an active session. They may have logged out and your application failed to
|
||||
remove the `my-refresh-token` cookie, or some other failure occurred that left
|
||||
a stale refresh token in the browser. Furthermore, a refresh token can only be
|
||||
used a few seconds after it was first used. Only use a refresh token if the
|
||||
access token is about to expire, which will avoid the introduction of difficult
|
||||
to diagnose logout bugs in your app.
|
||||
|
||||
A good practice is to handle unauthorized errors by deferring rendering the
|
||||
page in the browser instead of in the server. Some user information is
|
||||
contained in the access token though, so in certain cases, you may be able to
|
||||
use this potentially stale information to render a page.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### How do I make the cookies `HttpOnly`?
|
||||
|
||||
This is not necessary. Both the access token and refresh token are designed to
|
||||
be passed around to different components in your application. The browser-based
|
||||
side of your application needs access to the refresh token to properly maintain
|
||||
a browser session anyway.
|
||||
|
||||
### My server is getting invalid refresh token errors. What's going on?
|
||||
|
||||
It is likely that the refresh token sent from the browser to your server is
|
||||
stale. Make sure the `onAuthStateChange` listener callback is free of bugs and
|
||||
is registered relatively early in your application's lifetime.
|
||||
|
||||
When you receive this error on the server-side, try to defer
|
||||
rendering to the browser where the client library can access an up-to-date
|
||||
refresh token and present the user with a better experience.
|
||||
|
||||
### Should I set a shorter `Max-Age` parameter on the cookies?
|
||||
|
||||
The `Max-Age` or `Expires` cookie parameters only control whether the browser
|
||||
sends the value to the server. Since a refresh token represents the
|
||||
long-lived authentication session of the user on that browser, setting a short
|
||||
`Max-Age` or `Expires` parameter on the cookies only results in a degraded
|
||||
user experience.
|
||||
|
||||
The only way to ensure that a user has logged out or their session has ended
|
||||
is to get the user's details with `getUser()`.
|
||||
|
||||
### What should I use for the `SameSite` property?
|
||||
|
||||
Make sure you [understand the behavior of the property in different
|
||||
situations](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
|
||||
as some properties can degrade the user experience.
|
||||
|
||||
A good default is to use `Lax` which sends cookies when users are
|
||||
navigating to your site. Cookies typically require the `Secure` attribute,
|
||||
which only sends them over HTTPS. However, this can be a problem when
|
||||
developing on `localhost`.
|
||||
|
||||
### Can I use server-side rendering with a CDN or cache?
|
||||
|
||||
Yes, but you need to be careful to include at least the refresh token cookie
|
||||
value in the cache key. Otherwise you may be accidentally serving pages with
|
||||
data belonging to different users!
|
||||
|
||||
Also be sure you set proper cache control headers. We recommend invalidating
|
||||
cache keys every hour or less.
|
||||
@@ -107,4 +107,4 @@ brew upgrade supabase
|
||||
|
||||
- [Supabase CLI Reference](/docs/reference/cli/usage)
|
||||
- [Local Development](/docs/guides/cli/local-development)
|
||||
- [CI/CD Workflow](/docs/guides/cli/cicd-workflow)
|
||||
- [Managing Environments](/docs/guides/cli/managing-environments)
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: cicd-workflow
|
||||
title: CI / CD Workflow
|
||||
id: managing-environments
|
||||
title: Managing Environments
|
||||
description: How to deploy Supabase schema changes with a CI / CD pipeline.
|
||||
---
|
||||
|
||||
@@ -9,93 +9,83 @@ import TabItem from '@theme/TabItem'
|
||||
|
||||
## Overview
|
||||
|
||||
The Supabase CLI also functions as a database migrations tool. In this guide, we will show you how to setup your local Supabase development environment that integrates with GitHub Action to automatically test and release schema changes to staging and production Supabase projects.
|
||||
The Supabase CLI provides the tools you need to manage multiple environments.
|
||||
This guide shows you how to set up your local Supabase development environment that integrates with GitHub Actions to automatically
|
||||
test and release schema changes to staging and production Supabase projects.
|
||||
|
||||
- `develop` branch tracks staging
|
||||
- `main` branch tracks production
|
||||
|
||||

|
||||
|
||||
Before you start, here are some prerequisites:
|
||||
To get started:
|
||||
|
||||
- [Install the Supabase CLI](/docs/guides/cli)
|
||||
- Create a Supabase project
|
||||
- Initialise a local Git repository
|
||||
- Create a [Supabase project](https://app.supabase.com) or use an existing one
|
||||
- Initialize a local Git repository
|
||||
|
||||
You can use your existing Supabase project and Git repository to follow this guide. Otherwise, you can [create a new project](https://app.supabase.com/?next=new-project) and set up an empty Git repository.
|
||||
## Set up a local environment
|
||||
|
||||
## Setting up a project
|
||||
|
||||
The first step is to set up your local repository with the Supabase CLI. You can do this by running:
|
||||
The first step is to set up your local repository with the Supabase CLI:
|
||||
|
||||
```bash
|
||||
supabase init
|
||||
```
|
||||
|
||||
You should see that a new `supabase` directory is created. Then you need to link your local repository with your Supabase project:
|
||||
You should see a new `supabase` directory. Then you need to link your local repository with your Supabase project:
|
||||
|
||||
```sql
|
||||
```bash
|
||||
supabase login
|
||||
supabase link --project-ref $PROJECT_ID
|
||||
```
|
||||
|
||||
You can get your `$PROJECT_ID` from your project’s dashboard URL:
|
||||
You can get your `$PROJECT_ID` from your project's dashboard URL:
|
||||
|
||||
```
|
||||
https://app.supabase.com/project/<project-id>
|
||||
```
|
||||
|
||||
If you’re using an existing Supabase project, you might have made schema changes through the dashboard. You need to pull these changes before making local schema changes from the CLI. You can do this by running:
|
||||
If you're using an existing Supabase project, you might have made schema changes through the Dashboard.
|
||||
Run the following command to pull these changes before making local schema changes from the CLI:
|
||||
|
||||
```sql
|
||||
supabase db remote commit
|
||||
```
|
||||
|
||||
This command creates a new migration in `supabase/migrations/<timestamp>_remote_commit.sql` that reflects all the schema changes you might have made beforehand.
|
||||
This command creates a new migration in `supabase/migrations/<timestamp>_remote_commit.sql` which reflects the schema changes you have made previously.
|
||||
|
||||
Now commit your local changes to Git and run the local development setup:
|
||||
|
||||
```sql
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "init supabase"
|
||||
supabase start
|
||||
```
|
||||
|
||||
You are now ready to develop schema changes locally and create your first migration 🎉
|
||||
You are now ready to develop schema changes locally and create your first migration.
|
||||
|
||||
## Creating a new migration
|
||||
## Create a new migration
|
||||
|
||||
There are two ways to make schema changes in a version controlled way.
|
||||
There are two ways to make schema changes:
|
||||
|
||||
1. Write DDL statements manually into a migration file
|
||||
2. Make changes through Studio UI and auto generate a schema diff
|
||||
|
||||
For this guide, we will create an `employees` table using the schema below.
|
||||
|
||||
```sql
|
||||
create table public.employees (
|
||||
id integer primary key generated always as identity,
|
||||
name text
|
||||
);
|
||||
```
|
||||
1. Manual migration: Write DDL statements manually into a migration file
|
||||
1. Auto schema diff: Make changes through Studio UI and auto generate a schema diff
|
||||
|
||||
### Manual migration
|
||||
|
||||

|
||||
<!--  -->
|
||||
|
||||
The first step is to create a new migration script. You can do this by running:
|
||||
Create a new migration script by running:
|
||||
|
||||
```bash
|
||||
supabase migration new new_employee
|
||||
```
|
||||
|
||||
You should see that a new file `supabase/migrations/<timestamp>_new_employee.sql` is created. You can then write any DDL statements in this script using a text editor.
|
||||
You should see a new file created: `supabase/migrations/<timestamp>_new_employee.sql`. You can then write SQL statements in this script using a text editor:
|
||||
|
||||
Alternatively, the new migration command also supports stdin as input. This allows you to pipe in an existing script from another file or stdout.
|
||||
|
||||
```bash
|
||||
supabase migration new new_employee < create_employees_table.sql
|
||||
```sql
|
||||
create table public.employees (
|
||||
id integer primary key generated always as identity,
|
||||
name text
|
||||
);
|
||||
```
|
||||
|
||||
Next, you want to apply the new migration to your local database. You can do so by running:
|
||||
Apply the new migration to your local database:
|
||||
|
||||
```bash
|
||||
supabase db reset
|
||||
@@ -103,13 +93,21 @@ supabase db reset
|
||||
|
||||
This command recreates your local database from scratch and applies all migration scripts under `supabase/migrations` directory. Now your local database is up to date.
|
||||
|
||||
:::tip
|
||||
|
||||
The new migration command also supports stdin as input.
|
||||
This allows you to pipe in an existing script from another file or stdout:
|
||||
|
||||
`supabase migration new new_employee < create_employees_table.sql`
|
||||
:::
|
||||
|
||||
### Auto schema diff
|
||||
|
||||
The key difference between manual migration is that auto schema diff creates a new migration script from changes **already** applied to your local database.
|
||||
Unlike manual migrations, auto schema diff creates a new migration script from changes **already** applied to your local database.
|
||||
|
||||

|
||||
<!--  -->
|
||||
|
||||
The first step is to create an `employees` table under the `public` schema using Studio UI (accessible at [http://localhost:54323](http://localhost:54323/) by default).
|
||||
Create an `employees` table under the `public` schema using Studio UI, accessible at [localhost:54323](http://localhost:54323/) by default.
|
||||
|
||||
Next, generate a schema diff by running the following command:
|
||||
|
||||
@@ -146,30 +144,38 @@ GRANT ALL ON TABLE public.employees TO postgres;
|
||||
GRANT ALL ON TABLE public.employees TO service_role;
|
||||
```
|
||||
|
||||
You may notice that the auto generated migration script is more verbose than the manually written one. This is because the schema diff tool we use under the hood does not take into account of default privileges added by the initial schema.
|
||||
You may notice that the auto-generated migration script is more verbose than the manually written one.
|
||||
This is because the default schema diff tool does not account for default privileges added by the initial schema.
|
||||
|
||||
Alternatively, you may pass in the `--use-migra` experimental flag to generate a more concise schema diff.
|
||||
Commit the new migration script to git and you are ready to deploy.
|
||||
|
||||
```sql
|
||||
supabase db diff --use-migra
|
||||
```
|
||||
:::tip
|
||||
|
||||
Without the `-f` file flag, the output will be written to stdout by default.
|
||||
Alternatively, you may pass in the `--use-migra` experimental flag to generate a more concise migration using [migra](https://github.com/djrobstep/migra).
|
||||
Without the `-f` file flag, the output is written to stdout by default.
|
||||
|
||||
Finally, commit the new migration script to git and you are ready to deploy 🎉
|
||||
`supabase db diff --use-migra`
|
||||
|
||||
## Deploying a migration
|
||||
:::
|
||||
|
||||
In a real project, you might not want to deploy migrations directly from your local machine as other engineers could be pushing features simultaneously. Instead, you would use a CI/CD pipeline to deploy new migrations.
|
||||
## Deploy a migration
|
||||
|
||||

|
||||
In a production environment, we recommend using a CI/CD pipeline to deploy new migrations with GitHub Actions rather than deploying from your local machine.
|
||||
|
||||
In this section, we will show how to set up a CI/CD workflow with the CLI on GitHub Actions. We will use two projects here, one for production and one for staging. The prerequisites are:
|
||||

|
||||
|
||||
- Create separate Supabase projects for staging and production
|
||||
- Push your Git repository to GitHub and enable GitHub Actions
|
||||
This example uses two Supabase projects, one for production and one for staging.
|
||||
|
||||
> ⚠️ You need a _new_ project for staging. A project which has already been modified to reflect the production project’s schema can’t be used because the CLI would reapply these changes.
|
||||
Prepare your environments by:
|
||||
|
||||
- Creating separate Supabase projects for staging and production
|
||||
- Pushing your git repository to GitHub and enabling GitHub Actions
|
||||
|
||||
:::caution
|
||||
|
||||
You need a _new_ project for staging. A project which has already been modified to reflect the production project's schema can't be used because the CLI would reapply these changes.
|
||||
|
||||
:::
|
||||
|
||||
### Configure GitHub Actions
|
||||
|
||||
@@ -178,13 +184,20 @@ The Supabase CLI requires a few environment variables to run in non-interactive
|
||||
- `SUPABASE_ACCESS_TOKEN` is your personal access token
|
||||
- `SUPABASE_DB_PASSWORD` is your project specific database password
|
||||
|
||||
We recommend adding these as [encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) to your GitHub Action runners.
|
||||
We recommend adding these as [encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) to your GitHub Actions runners.
|
||||
|
||||
Here are the workflow files we will be using for this guide:
|
||||
Create the following files inside the `.github/workflows` directory:
|
||||
|
||||
- `.github/workflows/ci.yml`:
|
||||
<Tabs
|
||||
defaultValue="CI"
|
||||
values={[
|
||||
{label: 'ci.yaml', value: 'CI'},
|
||||
{label: 'staging.yaml', value: 'STAGING'},
|
||||
{label: 'production.yaml', value: 'PRODUCTION'},
|
||||
]}>
|
||||
<TabItem value="CI">
|
||||
|
||||
```yaml
|
||||
```yaml title=".github/workflows/ci.yml"
|
||||
name: CI
|
||||
|
||||
on:
|
||||
@@ -214,41 +227,10 @@ jobs:
|
||||
fi
|
||||
```
|
||||
|
||||
- `.github/workflows/production.yml`:
|
||||
</TabItem>
|
||||
<TabItem value="STAGING">
|
||||
|
||||
```yaml
|
||||
name: Deploy Migrations to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
|
||||
PRODUCTION_PROJECT_ID: abcdefghijklmnopqrst
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: 1.0.0
|
||||
|
||||
- run: |
|
||||
supabase link --project-ref $PRODUCTION_PROJECT_ID
|
||||
supabase db push
|
||||
```
|
||||
|
||||
- `.github/workflows/staging.yml`:
|
||||
|
||||
```yaml
|
||||
```yaml title=".github/workflows/staging.yml"
|
||||
name: Deploy Migrations to Staging
|
||||
|
||||
on:
|
||||
@@ -278,7 +260,45 @@ jobs:
|
||||
supabase db push
|
||||
```
|
||||
|
||||
Commit these files to Git and push to your `main` branch on GitHub. Make sure you’ve updated these environment variables to match your Supabase projects:
|
||||
</TabItem>
|
||||
<TabItem value="PRODUCTION">
|
||||
|
||||
```yaml title=".github/workflows/production.yml"
|
||||
name: Deploy Migrations to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
|
||||
PRODUCTION_PROJECT_ID: abcdefghijklmnopqrst
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: 1.0.0
|
||||
|
||||
- run: |
|
||||
supabase link --project-ref $PRODUCTION_PROJECT_ID
|
||||
supabase db push
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The full example code is available in the [demo repository](https://github.com/supabase/supabase-action-example).
|
||||
|
||||
Commit these files to git and push to your `main` branch on GitHub. Update these environment variables to match your Supabase projects:
|
||||
|
||||
- `SUPABASE_ACCESS_TOKEN`
|
||||
- `PRODUCTION_PROJECT_ID`
|
||||
@@ -286,13 +306,13 @@ Commit these files to Git and push to your `main` branch on GitHub. Make sure yo
|
||||
- `STAGING_PROJECT_ID`
|
||||
- `STAGING_DB_PASSWORD`
|
||||
|
||||
When configured correctly, your repository should have CI and Release workflows that trigger on new commits pushed to `main` and `develop` branches.
|
||||
When configured correctly, your repository will have CI and Release workflows that trigger on new commits pushed to `main` and `develop` branches.
|
||||
|
||||

|
||||
|
||||
### Open a PR with new migration
|
||||
|
||||
Now that your repository is set up, it’s time to create a new migration. Follow the [migration section](#creating-a-new-migration) to create a file `supabase/migrations/<timestamp>_new_employee.sql`.
|
||||
Follow the [migration steps](#create-a-new-migration) to create a `supabase/migrations/<timestamp>_new_employee.sql` file.
|
||||
|
||||
Checkout a new branch `feat/employee` from `develop` , commit the migration file, and push to GitHub.
|
||||
|
||||
@@ -303,26 +323,18 @@ git commit -m "Add employee table"
|
||||
git push --set-upstream origin feat/employee
|
||||
```
|
||||
|
||||
Then open a PR from `feat/employee` to `develop` branch.
|
||||
Open a PR from `feat/employee` to the `develop` branch to see that the CI workflow has been triggered.
|
||||
|
||||

|
||||
|
||||
You can see that this triggers the test job on GitHub Actions. You can add more test steps to the job, such as running integration tests against the local database started by Supabase CLI. Here we assert that the generated types are up-to-date with new schema.
|
||||
|
||||

|
||||
|
||||
Once the test error are resolved, merge this PR, and watch the deployment in action 🚀
|
||||
Once the test error is resolved, merge this PR and watch the deployment in action.
|
||||
|
||||
### Release to production
|
||||
|
||||
After verifying your staging project has successfully migrated, create another PR from `develop` to `main` and merge it to deploy the migration to the production project.
|
||||
|
||||

|
||||
<!--  -->
|
||||
|
||||
The `release` job applies all new migration scripts merged in `supabase/migrations` directory to a linked Supabase project. You can control which project the job links to via `PROJECT_ID` environment variable.
|
||||
|
||||
The full example code for this guide is available on our [demo repository](https://github.com/supabase/supabase-action-example).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sync production project to staging
|
||||
@@ -388,7 +400,7 @@ Apart from tables, you also need to reassign owner of other entities using their
|
||||
|
||||
Sometimes your teammate may merge a new migration file to git main branch, and now you need to rebase your local schema changes on top.
|
||||
|
||||

|
||||
<!--  -->
|
||||
|
||||
We can handle this scenario gracefully by renaming your old migration file with a new timestamp.
|
||||
|
||||
@@ -33,15 +33,6 @@ Let's cover some basic concepts:
|
||||
2. Click on **Extensions** in the sidebar.
|
||||
3. Search for "pgtap" and enable the extension.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="/docs/videos/toggle-extensions.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
@@ -62,30 +53,6 @@ It's good practice to create the extension within a separate schema (like `exten
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Managing tests
|
||||
|
||||
It's a good practice to keep all your tests in a separate schema.
|
||||
|
||||
```sql
|
||||
create schema tests;
|
||||
```
|
||||
|
||||
### Creating a test
|
||||
|
||||
@TODO
|
||||
|
||||
- Create a plan
|
||||
- We should come up with a recommendation on how to run the tests. Via a function? External scripts?
|
||||
- Eventually this can be done via our CLI
|
||||
|
||||
### Running a test
|
||||
|
||||
@TODO
|
||||
|
||||
## Examples
|
||||
|
||||
Let's look at a few different tests which could be helpful in your project.
|
||||
|
||||
### Testing tables
|
||||
|
||||
```sql
|
||||
@@ -100,7 +67,9 @@ rollback;
|
||||
|
||||
API:
|
||||
|
||||
- [`has_table()`](https://pgtap.org/documentation.html#has_table)
|
||||
- [`has_table()`](https://pgtap.org/documentation.html#has_table): Tests whether or not a table exists in the database
|
||||
- [`has_index()`](https://pgtap.org/documentation.html#has_index): Checks for the existence of a named index associated with the named table.
|
||||
- [`has_relation()`](https://pgtap.org/documentation.html#has_relation): Tests whether or not a relation exists in the database.
|
||||
|
||||
### Testing columns
|
||||
|
||||
@@ -108,8 +77,8 @@ API:
|
||||
begin;
|
||||
select plan( 2 );
|
||||
|
||||
select has_column( 'profiles', 'id' );
|
||||
select col_is_pk( 'profiles', 'id' );
|
||||
select has_column( 'profiles', 'id' ); # test that the "id" column exists in the "profiles" table
|
||||
select col_is_pk( 'profiles', 'id' ); # test that the "id" column is a primary key
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
@@ -117,8 +86,74 @@ rollback;
|
||||
|
||||
API:
|
||||
|
||||
- [`has_column()`](https://pgtap.org/documentation.html#has_column)
|
||||
- [`col_is_pk()`](https://pgtap.org/documentation.html#col_is_pk)
|
||||
- [`has_column()`](https://pgtap.org/documentation.html#has_column): Tests whether or not a column exists in a given table, view, materialized view or composite type.
|
||||
- [`col_is_pk()`](https://pgtap.org/documentation.html#col_is_pk): Tests whether the specified column or columns in a table is/are the primary key for that table.
|
||||
|
||||
### Testing RLS Policies
|
||||
|
||||
```sql
|
||||
begin;
|
||||
select plan( 1 );
|
||||
|
||||
select policies_are(
|
||||
'public',
|
||||
'profiles',
|
||||
ARRAY [
|
||||
'Profiles are public', # Test that there is a policy called "Profiles are public" on the "profiles" table.
|
||||
'Profiles can only be updated by the owner' # Test that there is a policy called "Profiles can only be updated by the owner" on the "profiles" table.
|
||||
]
|
||||
);
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- [`policies_are()`](https://pgtap.org/documentation.html#policies_are): Tests that all of the policies on the named table are only the policies that should be on that table.
|
||||
- [`policy_roles_are()`](https://pgtap.org/documentation.html#policy_roles_are): Tests whether the roles to which policy applies are only the roles that should be on that policy.
|
||||
- [`policy_cmd_is()`](https://pgtap.org/documentation.html#policy_cmd_is): Tests whether the command to which policy applies is same as command that is given in function arguments.
|
||||
|
||||
You can also use the `results_eq()` method to test that a Policy returns the correct data:
|
||||
|
||||
```sql
|
||||
begin;
|
||||
select plan( 1 );
|
||||
|
||||
select results_eq(
|
||||
'select * from profiles()',
|
||||
$$VALUES ( 1, 'Anna'), (2, 'Bruce'), (3, 'Caryn')$$
|
||||
'profiles() should return all users'
|
||||
);
|
||||
|
||||
|
||||
select * from finish();
|
||||
rollback;
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
- [`results_eq()`](https://pgtap.org/documentation.html#results_eq)
|
||||
- [`results_ne()`](https://pgtap.org/documentation.html#results_ne)
|
||||
|
||||
### Testing Functions
|
||||
|
||||
```sql
|
||||
begin;
|
||||
select plan( 1 );
|
||||
|
||||
select function_returns( 'hello_world', 'text' ); # test if the function "hello_world" returns text
|
||||
select function_returns( 'is_even', ARRAY['integer'], 'boolean' ); # test if the function "is_even" returns a boolean
|
||||
select results_eq('select * from hello_world()', 'hello'); # test if the function "hello_world" returns "hello"
|
||||
|
||||
select * from finish();
|
||||
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).
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ GRANT EXECUTE ON FUNCTION hello_world TO service_role;
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Dart](../../reference/dart/rpc)
|
||||
- Official Client libraries: [JavaScript](../../reference/javascript/rpc) and [Flutter](../../reference/dart/rpc)
|
||||
- Community client libraries: [github.com/supabase-community](https://github.com/supabase-community)
|
||||
- PostgreSQL Official Docs: [Chapter 9. Functions and Operators](https://www.postgresql.org/docs/current/functions.html)
|
||||
- PostgreSQL Reference: [CREATE FUNCTION](https://www.postgresql.org/docs/9.1/sql-createfunction.html)
|
||||
|
||||
@@ -4,7 +4,9 @@ title: Examples and Resources
|
||||
description: 'Examples you can use to get started with Supabase'
|
||||
---
|
||||
|
||||
We have a [set of examples](https://github.com/supabase/examples) in our [main repository](https://github.com/supabase/supabase) to help you get started.
|
||||
<!-- This page is outdated and should not be added to the sidebar until it has been reworked -->
|
||||
|
||||
We have a [set of examples](https://github.com/supabase/supabase/tree/master/examples) in our [main repository](https://github.com/supabase/supabase) to help you get started.
|
||||
|
||||
## Featured
|
||||
|
||||
@@ -56,7 +58,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- [Expo Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/expo-todo-list)
|
||||
- [Next.js Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/nextjs-todo-list)
|
||||
- [React Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/react-todo-list)
|
||||
- [Svelte Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/sveltejs-todo-list)
|
||||
- [Svelte Todo List.](https://github.com/supabase/supabase/tree/master/examples/todo-list/sveltejs-todo-list)
|
||||
- [Vue 3 Todo List (Typescript).](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/vue3-ts-todo-list)
|
||||
- [Angular Todo List.](https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/angular-todo-list)
|
||||
- [Nuxt 3 Todo List.](https://github.com/nuxt-community/supabase-module/tree/main/demo)
|
||||
@@ -64,7 +66,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
### Auth examples
|
||||
|
||||
- [Supabase Auth with vanilla JavaScript.](https://github.com/supabase/examples/tree/main/supabase-js-v1/auth/javascript-auth). Use Supabase without any frontend frameworks.
|
||||
- [Supabase Auth with Next.js SSR.](https://github.com/supabase/examples/tree/main/supabase-js-v1/nextjs-with-supabase-auth) Uses cookies to persist auth between the server and the client.
|
||||
- [Supabase Auth with Next.js](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
- [Supabase Auth with RedwoodJS.](https://redwood-playground-auth.netlify.app/supabase) Try out Supabase authentication in the [RedwoodJS](https://redwoodjs.com) Authentication Playground complete with OAuth support and code samples.
|
||||
|
||||
### Collaborative
|
||||
@@ -80,7 +82,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
### Libraries
|
||||
|
||||
- D (in development): [GitHub](https://github.com/csharpdf/dupabase)
|
||||
- Dart (in development): [GitHub](https://github.com/supabase/supabase-dart)
|
||||
- Flutter (in development): [GitHub](https://github.com/supabase/supabase-flutter)
|
||||
- Python (in development): [GitHub](https://github.com/supabase/supabase-py)
|
||||
- C# (in development): [GitHub](https://github.com/supabase/supabase-csharp)
|
||||
- Kotlin (in development): [GitHub](https://github.com/supabase-community/postgrest-kt)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
id: features
|
||||
title: Features
|
||||
description: 'Supabase features'
|
||||
---
|
||||
|
||||
This is a non-exhaustive list of features that Supabase provides for every project.
|
||||
|
||||
## Database
|
||||
|
||||
### Postgres Database
|
||||
|
||||
Every project is a full Postgres database. [Docs](/docs/guides/database).
|
||||
|
||||
### Database Extensions
|
||||
|
||||
Every database comes with a full set of Postgres extensions. [Docs](/docs/guides/database/extensions).
|
||||
|
||||
### Database Functions
|
||||
|
||||
Create custom database functions which you can call from the browser. [Docs](/docs/guides/database/functions).
|
||||
|
||||
### Database Triggers
|
||||
|
||||
Attach triggers to your tables to handle database changes. [Docs](/docs/guides/auth/managing-user-data#using-triggers).
|
||||
|
||||
### Database Webhooks
|
||||
|
||||
Send database changes to any external service using Webhooks. [Link](https://supabase.com/blog/supabase-functions-updates#database-webhooks-alpha).
|
||||
|
||||
### Database Backups
|
||||
|
||||
Projects are backed up daily with the option to upgrade to Point in Time recovery.
|
||||
|
||||
### Search
|
||||
|
||||
Build search functionality using Postgres Full Text Search. [Docs](/docs/guides/database/full-text-search).
|
||||
|
||||
### Secrets and encryption
|
||||
|
||||
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Link](https://supabase.com/blog/supabase-vault).
|
||||
|
||||
### Database migrations
|
||||
|
||||
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/cli/local-development#database-migrations)
|
||||
|
||||
<br />
|
||||
|
||||
## Auth
|
||||
|
||||
### Email & Password Logins
|
||||
|
||||
Build email logins for your application or website. [Docs](/docs/guides/auth/auth-email).
|
||||
|
||||
### Magic Links
|
||||
|
||||
Build passwordless logins for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
|
||||
|
||||
### Social Logins
|
||||
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/auth-apple).
|
||||
|
||||
### Phone Logins
|
||||
|
||||
Provide phone logins using a 3rd-party SMS provider. [Docs](/docs/guides/auth/auth-twilio).
|
||||
|
||||
### Row Level Security
|
||||
|
||||
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
|
||||
|
||||
### Serverside 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)
|
||||
|
||||
### Auth UI Kit
|
||||
|
||||
Build login and registration pages with custom themes. [Docs](/docs/guides/auth/auth-helpers/auth-ui).
|
||||
|
||||
<br />
|
||||
|
||||
## APIs & Client libraries
|
||||
|
||||
### Auto-generated REST API
|
||||
|
||||
RESTful APIs are autogenerated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
|
||||
|
||||
### Auto-generated GraphQL API
|
||||
|
||||
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/api#graphql-api-overview).
|
||||
|
||||
### Realtime Database changes
|
||||
|
||||
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-cdc).
|
||||
|
||||
### User Broadcasting
|
||||
|
||||
Send messages between connected users through websockets. [Docs](/docs/guides/realtime#broadcast).
|
||||
|
||||
### User Presence
|
||||
|
||||
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime#presence).
|
||||
|
||||
### Client libraries
|
||||
|
||||
Official client libraries for [JavaScript](/docs/reference/javascript/) and [Dart](/docs/reference/dart).
|
||||
Unofficial libraries [supported by the community](https://github.com/supabase-community#client-libraries).
|
||||
|
||||
<br />
|
||||
|
||||
## File Storage
|
||||
|
||||
### Large File storage
|
||||
|
||||
Supabase Storage makes it simple to store and serve large files. [Docs](/docs/guides/storage).
|
||||
|
||||
### Storage CDN
|
||||
|
||||
Cache large files using the Supabase CDN. [Docs](/docs/guides/storage-cdn).
|
||||
|
||||
<br />
|
||||
|
||||
## Edge Functions
|
||||
|
||||
### Deno Edge Functions
|
||||
|
||||
Globally distributed TypeScript functions to execute custom business logic. [Docs](/docs/guides/functions).
|
||||
|
||||
<br />
|
||||
|
||||
## Project Managment
|
||||
|
||||
### CLI
|
||||
|
||||
Use our CLI to develop your project locally and deploy to the Supabase Platform. [Docs](/docs/reference/cli).
|
||||
|
||||
### Admin API
|
||||
|
||||
Manage your projects programmatically. [Docs](/docs/reference/api).
|
||||
|
||||
<br />
|
||||
|
||||
## Feature Status
|
||||
|
||||
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
|
||||
|
||||
| Product | Feature | Stage |
|
||||
| -------------------------- | ---------------------- | ------- |
|
||||
| Database | Postgres | `GA` |
|
||||
| Database | Triggers | `GA` |
|
||||
| Database | Functions | `GA` |
|
||||
| Database | Extensions | `GA` |
|
||||
| Database | Full Text Search | `GA` |
|
||||
| Database | Webhooks | `alpha` |
|
||||
| Database | Point-in-Time Recovery | `alpha` |
|
||||
| Database | Vault | `alpha` |
|
||||
| Studio | | `GA` |
|
||||
| Realtime | Postgres CDC | `beta` |
|
||||
| Realtime | Broadcast | `alpha` |
|
||||
| Realtime | Presence | `alpha` |
|
||||
| Storage | Backend (S3) | `GA` |
|
||||
| Storage | API | `beta` |
|
||||
| Storage | CDN | `beta` |
|
||||
| Edge Functions | | `beta` |
|
||||
| Auth | OAuth Providers | `beta` |
|
||||
| Auth | Passwordless | `beta` |
|
||||
| Auth | Next.js Auth Helpers | `alpha` |
|
||||
| Auth | SvelteKit Auth Helpers | `alpha` |
|
||||
| Admin API | | `beta` |
|
||||
| CLI | | `beta` |
|
||||
| Client Library: JavaScript | | `GA` |
|
||||
| Client Library: Dart | | `beta` |
|
||||
@@ -4,7 +4,7 @@ title: CI / CD Workflow
|
||||
description: How to deploy Supabase Edge Functions with a CI / CD pipeline.
|
||||
---
|
||||
|
||||
As described in the Supabase CLI [CI / CD Workflow documentation](/docs/guides/cli/cicd-workflow), you can use the [`setup-cli` GitHub Action](https://github.com/marketplace/actions/supabase-cli-action) to run Supabase CLI commands in your GitHub Actions, for example to deploy a Supabase Edge Function:
|
||||
As described in the Supabase CLI [Environments Guide](/docs/guides/cli/managing-environments), you can use the [`setup-cli` GitHub Action](https://github.com/marketplace/actions/supabase-cli-action) to run Supabase CLI commands in your GitHub Actions, for example to deploy a Supabase Edge Function:
|
||||
|
||||
```yaml
|
||||
name: Deploy Function
|
||||
|
||||
@@ -95,6 +95,11 @@ POSTGRES_PORT=5432
|
||||
|
||||
Once you have done this, you can safely comment out the `db` section of the `docker-compose` file, and remove any instances where the services `depends_on` the `db` image.
|
||||
|
||||
### Setting database's `log_min_messages`
|
||||
|
||||
By default, `docker-compose` sets the database's `log_min_messages` configuration to `fatal` to prevent redundant logs generated by Realtime.
|
||||
However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs.
|
||||
|
||||
## Deploying
|
||||
|
||||
See the following guides to deploy Docker Compose setup using your preferred tool and platform:
|
||||
|
||||
@@ -36,7 +36,7 @@ Each system has a number of configuration options which can be found in the rele
|
||||
|
||||
- [Postgres](https://hub.docker.com/_/postgres/)
|
||||
- [PostgREST](https://postgrest.org/en/stable/configuration.html)
|
||||
- [Realtime](https://github.com/supabase/realtime#server-set-up)
|
||||
- [Realtime](https://github.com/supabase/realtime#server)
|
||||
- [GoTrue](https://github.com/supabase/gotrue)
|
||||
- [Storage](https://github.com/supabase/storage-api)
|
||||
- [Kong](https://docs.konghq.com/install/docker/)
|
||||
@@ -46,7 +46,7 @@ Each system has a number of configuration options which can be found in the rele
|
||||
It is recommended that you decouple your database from the middleware so that you can upgrade the middleware without any downtime.
|
||||
The "middleware" is everything except Postgres, and it should work with any Postgres provider (such as AWS RDS), or your own Postgres cluster.
|
||||
|
||||
### Database Extensions
|
||||
### Extensions
|
||||
|
||||
Supabase requires some Postgres extensions to be enabled by default for the API and Auth system to work. You can find the extensions inside the
|
||||
[schema initialization script](https://github.com/supabase/supabase/blob/master/docker/volumes/db/init/00-initial-schema.sql).
|
||||
@@ -69,7 +69,7 @@ For UUID functions, required for PostgreSQL <13.
|
||||
|
||||
For working with JWT and Auth functions.
|
||||
|
||||
### Database Roles
|
||||
### Roles
|
||||
|
||||
Supabase creates several default roles in your Postgres database. To restore defaults at any time you can run the commands inside the
|
||||
[schema initialization script](https://github.com/supabase/supabase/blob/master/docker/volumes/db/init/00-initial-schema.sql).
|
||||
@@ -111,6 +111,10 @@ For running commands via the Supabase UI.
|
||||
|
||||
Supabase Administrative role for maintaining your database.
|
||||
|
||||
### Realtime Logs
|
||||
|
||||
Set your database's `log_min_messages` configuration to `fatal` to prevent redundant database logs generated by Realtime. However, you might miss important log messages such as database errors. Configure `log_min_messages` based on your needs.
|
||||
|
||||
## API Keys
|
||||
|
||||
The API Gateway (Kong) uses JWT to authenticate access through to the database. The JWT should correspond to a relevant Postgres Role,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
|
||||
id: keyri
|
||||
title: 'Keyri'
|
||||
description: 'QR authentication for an easy and flexible biometric solution across all platforms.'
|
||||
---
|
||||
|
||||
Keyri can be used to incorporate sign-in-with-QR functionality into your Supabase app, allowing users to scan a QR code on your web app with your mobile app and be instantly logged into the web app without having to input any credentials.
|
||||
|
||||
Configuration is split into Web and Mobile components. On web, the Keyri QR Widget needs to be installed along with an event listener, and in your mobile app, install the Keyri SDK and pass into it the user's refresh token when sign-in-with-QR is initiated. When the refresh token lands in your web app, it's passed into Supabase's `setSession()` method.
|
||||
|
||||
# Sign up for Keyri
|
||||
|
||||
First make a free account on the Keyri dashboard (<https://app.keyri.com>). On Add Your Application, set a name and input the domain on which your app will eventually be deployed. You can create multiple application in Keyri to account for your development, staging, and production environments
|
||||
|
||||

|
||||
|
||||
Note your application key from the Keys and Credentials section - this will be used in the Mobile portion of the implementation
|
||||
|
||||

|
||||
|
||||
# Web
|
||||
|
||||
[](http://www.youtube.com/watch?v=jrjrcpc2PFQ "Supabase & Keyri | Web Integration Overview")
|
||||
|
||||
For your web app, first download KeyriQR.html (available [here](https://raw.githubusercontent.com/Keyri-Co/library-keyri-connect/main/KeyriQR.html)) and save it to a public directory.
|
||||
|
||||
Next, embed KeyriQR.html in your login page as an iFrame within the desired div. This serves as the widget that displays the dynamic QR code and connects with the Keyri API.
|
||||
|
||||
```html
|
||||
<div>
|
||||
<iframe
|
||||
title='KeyriQR'
|
||||
src='/KeyriQR.html'
|
||||
id='qr-frame'
|
||||
height='300'
|
||||
width='300'
|
||||
scrolling='no'
|
||||
style={{ border: 'solid 5px white' }}
|
||||
></iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
Next, for the same login view, set up an event listener to pick up the session token that the iFrame emits when the QR code is scanned by your app.
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', async (evt) => {
|
||||
if (evt.data.keyri && evt.data.data && document.location.origin == evt.origin) {
|
||||
const { data } = evt;
|
||||
if (!data.error) {
|
||||
let refresh_token = JSON.parse(data.data).refreshToken;
|
||||
await handleQrLogin(refresh_token);
|
||||
} else if (data.error) {
|
||||
console.log(`Keyri error: ${data.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
# Mobile
|
||||
|
||||
[](http://www.youtube.com/watch?v=oGMsSKyh6tc "Supabase & Keyri | Mobile Integration Overview")
|
||||
|
||||
### Install Flutter
|
||||
|
||||
First, install the Flutter SDK, found at flutter.dev
|
||||
|
||||
Make sure to add Flutter to your PATH, for example: 
|
||||
|
||||
```shell
|
||||
export PATH="$PATH:`pwd`/flutter/bin"
|
||||
```
|
||||
|
||||
### Apple - initial setup
|
||||
|
||||
Download the latest version of Xcode from the Mac App Store. Make sure the Xcode provided simulator is using a 64-bit device (iPhone 5s or later). You can check the device by viewing the settings in the simulator’s **Hardware > Device** or **File > Open Simulator** menus.
|
||||
|
||||
### Android - initial setup
|
||||
|
||||
Download the latest version of [Android Studio](https://developer.android.com/studio). Install Android SDK and needed emulator(s).
|
||||
|
||||
### Create Project
|
||||
|
||||
Run this command in your terminal/shell at the desired location for your new project
|
||||
|
||||
```shell
|
||||
$ flutter create my_app
|
||||
```
|
||||
|
||||
You can then CD into the new directory, and run the test app with 
|
||||
|
||||
```shell
|
||||
flutter run
|
||||
```
|
||||
|
||||
This is a good test - if things are configured correctly so far you should see the default Flutter test app deployed.
|
||||
|
||||
### Add dependencies (Keyri and Supabase)
|
||||
|
||||
Open your Pubspec.yaml file, which should be at the top level directory in your new project
|
||||
|
||||
Add Keyri and Supabase under **dependencies**
|
||||
|
||||

|
||||
|
||||
One can now access Supabase and Keyri sdks in their Flutter code
|
||||
|
||||
### Utilize the two together
|
||||
|
||||
1. Make a request to Supabase to authenticate the user
|
||||
|
||||
2. Parse the response to extract the token
|
||||
|
||||
3. Authenticate using Keyri
|
||||
1. Below, we show how to utilize the EasyKeyriAuth function, which takes the user through scanning the code, creating the session, displaying the confirmation screen, and finalizing the payload transmission
|
||||
- Note - you can find your App Key in the Keyri Developer Portal
|
||||
|
||||
2. Alternatively, intermediate functions in the Keyri SDK, discussed in the mobile docs, can provide control over displaying a custom QR Scanner and/or Confirmation screen
|
||||
|
||||
```kotlin
|
||||
// Sign in user with email and password
|
||||
// Alternatively one can utilize the Supabase API to accomplish the same thing
|
||||
final response = await client.auth.signIn(email: 'email', password: 'password');
|
||||
|
||||
if (response.error != null) {
|
||||
// Error
|
||||
print('Error: ${response.error?.message}');
|
||||
} else {
|
||||
// Success
|
||||
final session = response.data;
|
||||
|
||||
// This is the payload that needs to be send through Keyri
|
||||
final refreshToken = session.refreshToken
|
||||
|
||||
// EasyKeyriAuth guides the user through scanning and parsing the QR, confirming the session, and configuring the payload
|
||||
// One can also use the initiateQRSession method to use the Keyri Scanner with a custom Confirmation screen
|
||||
// Or the ProcessLink method if you have your own scanner or are using deep linking
|
||||
await keyri
|
||||
.easyKeyriAuth([App Key],
|
||||
'{"refreshToken":"$refreshToken"}', [email])
|
||||
.then((authResult) => _onAuthResult(authResult))
|
||||
.catchError((error, stackTrace) => _onError(error));
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
id: custom-domains
|
||||
title: Custom Domains
|
||||
description: Configuring a Custom Domain for your Supabase project.
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
|
||||
:::note
|
||||
|
||||
Custom Domains are currently in beta, and are being slowly made available to projects.
|
||||
|
||||
:::
|
||||
|
||||
Custom domains allow you to present a branded experience to your users. You can also use a subdomain for the purposes of this guide.
|
||||
|
||||
To get started:
|
||||
|
||||
1. [Install](/docs/guides/cli) the Supabase CLI 1.10.1+.
|
||||
1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI.
|
||||
1. Enable the Custom Domains add-on in the Billing page of the Dashboard.
|
||||
|
||||
## Configure a Custom Domain
|
||||
|
||||
This example assumes your Supabase project is `foobar` with a corresponding API URL `foobar.supabase.co` and configures a custom domain at `api.example.com`.
|
||||
|
||||
### <a id="cname-configure"></a>Configure a CNAME
|
||||
|
||||
Set up a CNAME record for `api.example.com`, resolving to `foobarbaz.supabase.co.`, with as low a TTL as possible.
|
||||
|
||||
### Configure TXT Verification
|
||||
|
||||
Use the `create` subcommand of the CLI to notify Supabase of your domain and retrieve TXT verification records:
|
||||
|
||||
```bash
|
||||
supabase custom-hostname create --project-ref foobar --custom-hostname api.example.com --experimental
|
||||
```
|
||||
|
||||
The output of the `create` command includes two TXT records[^1] you will need to set up, in order to verify your control over the domain in question,
|
||||
and for us to issue SSL certificates for it. For example:
|
||||
|
||||
```
|
||||
[...]
|
||||
Required outstanding validation records:
|
||||
_cf-custom-hostname.api.example.com TXT -> 46BBC14D-D50A-409C-8DB5-F862CF5BA660
|
||||
api.example.com TXT -> ca3-F1HvR9i938OgVwpCFwi1jTsbhe1hvT0Ic3efPY3Q
|
||||
```
|
||||
|
||||
[^1] One of the records requires you to *replace* the CNAME record set up in [the first step](#cname-configure) with a TXT record. You'll be able to restore it back to the CNAME after the verification process has been completed.
|
||||
|
||||
### Verify your domain
|
||||
|
||||
Set up both records as instructed, and then use the `reverify` command for the Supabase Platform to verify the records:
|
||||
|
||||
```bash
|
||||
supabase custom-hostname reverify --project-ref foobarbaz --experimental
|
||||
```
|
||||
|
||||
You might need to wait a few minutes before your updated DNS records are propagated, especially if the older records were using a high TTL.
|
||||
|
||||
### Activate your domain
|
||||
|
||||
The final activation step reconfigures your project to start serving traffic on your custom domain (`api.example.com`).
|
||||
The auth service, in particular, will no longer work with the original URL (`foobar.supabase.co`).
|
||||
As such, it is recommended that you schedule a downtime window of 20-30 minutes, depending on the complexity of your project, to update all the services that need to know about your custom domain:
|
||||
|
||||
- any client code (e.g., frontends, mobile apps)
|
||||
- any OAuth providers (e.g., google, github)
|
||||
|
||||
Additionally, update the DNS configuration for `api.example.com` to once more use a CNAME record that resolves to `foobarbaz.supabase.co`.
|
||||
Finally, you can use the `activate` subcommand to reconfigure your project:
|
||||
|
||||
```bash
|
||||
supabase custom-hostname activate --project-ref foobarbaz --experimental
|
||||
```
|
||||
|
||||
## Remove a Custom Domain
|
||||
|
||||
If you have a custom domain (`api.example.com`) set up for your Supabase project (ref `foobarbaz`, with assigned endpoints at `foobarbaz.supabase.co`), and would like to go back to using the Supabase-provisioned endpoints (`foobarbaz.supabase.co`), you can use the `delete` subcommand:
|
||||
|
||||
```bash
|
||||
supabase custom-hostname delete --project-ref foobarbaz --experimental
|
||||
```
|
||||
|
||||
As with the final activation stage of the process for setting up a custom domain, you'll need to update any references in your client code and OAuth providers from the custom domain to the Supabase-provisioned endpoints.
|
||||
@@ -38,7 +38,7 @@ keep all video files in a "video" bucket, and profile pictures in an "avatar" bu
|
||||
## Getting started
|
||||
|
||||
This is a quick guide that shows the basic functionality of Supabase Storage. Find a full
|
||||
[example application in GitHub](https://github.com/supabase/examples/tree/main/supabase-js-v1/user-management/nextjs-ts-user-management),
|
||||
[example application in GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management),
|
||||
which you can deploy yourself.
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
|
||||
|
||||
@@ -7,143 +7,18 @@ sidebar_label: Angular
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Angular. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started!
|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/angular-user-management).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -180,7 +55,7 @@ export const environment = {
|
||||
Now that we have the API credentials in place, let's create a **SupabaseService** with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API.
|
||||
|
||||
```ts title="src/app/supabase.service.ts"
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
AuthChangeEvent,
|
||||
AuthSession,
|
||||
@@ -188,36 +63,36 @@ import {
|
||||
Session,
|
||||
SupabaseClient,
|
||||
User,
|
||||
} from '@supabase/supabase-js';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Database } from 'src/schema';
|
||||
} from '@supabase/supabase-js'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { Database } from 'src/schema'
|
||||
|
||||
export interface Profile {
|
||||
id?: string;
|
||||
username: string;
|
||||
website: string;
|
||||
avatar_url: string;
|
||||
id?: string
|
||||
username: string
|
||||
website: string
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient;
|
||||
_session: AuthSession | null = null;
|
||||
private supabase: SupabaseClient
|
||||
_session: AuthSession | null = null
|
||||
|
||||
constructor() {
|
||||
this.supabase = createClient(
|
||||
environment.supabaseUrl,
|
||||
environment.supabaseKey
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
get session() {
|
||||
this.supabase.auth.getSession().then(({ data }) => {
|
||||
this._session = data.session;
|
||||
this._session = data.session
|
||||
})
|
||||
return this._session;
|
||||
return this._session
|
||||
}
|
||||
|
||||
profile(user: User) {
|
||||
@@ -225,43 +100,43 @@ export class SupabaseService {
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
.single()
|
||||
}
|
||||
|
||||
authChanges(
|
||||
callback: (event: AuthChangeEvent, session: Session | null) => void
|
||||
) {
|
||||
return this.supabase.auth.onAuthStateChange(callback);
|
||||
return this.supabase.auth.onAuthStateChange(callback)
|
||||
}
|
||||
|
||||
signIn(email: string) {
|
||||
return this.supabase.auth.signInWithOtp({ email });
|
||||
return this.supabase.auth.signInWithOtp({ email })
|
||||
}
|
||||
|
||||
signOut() {
|
||||
return this.supabase.auth.signOut();
|
||||
return this.supabase.auth.signOut()
|
||||
}
|
||||
|
||||
updateProfile(profile: Profile) {
|
||||
const update = {
|
||||
...profile,
|
||||
updated_at: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
return this.supabase.from('profiles').upsert(update);
|
||||
return this.supabase.from('profiles').upsert(update)
|
||||
}
|
||||
|
||||
downLoadImage(path: string) {
|
||||
return this.supabase.storage.from('avatars').download(path);
|
||||
return this.supabase.storage.from('avatars').download(path)
|
||||
}
|
||||
|
||||
uploadAvatar(filePath: string, file: File) {
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file);
|
||||
return this.supabase.storage.from('avatars').upload(filePath, file)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
Optionally, update [src/styles.css](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
@@ -269,9 +144,9 @@ Let's set up an Angular component to manage logins and sign ups. We'll use Magic
|
||||
Create an **AuthComponent** with `ng g c auth` Angular CLI command.
|
||||
|
||||
```ts title="src/app/auth/auth.component.ts"
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-auth',
|
||||
@@ -279,11 +154,11 @@ import { SupabaseService } from '../supabase.service';
|
||||
styleUrls: ['./auth.component.css'],
|
||||
})
|
||||
export class AuthComponent implements OnInit {
|
||||
loading = false;
|
||||
loading = false
|
||||
|
||||
signInForm = this.formBuilder.group({
|
||||
email: '',
|
||||
});
|
||||
})
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
@@ -294,18 +169,18 @@ export class AuthComponent implements OnInit {
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
const email = this.signInForm.value.email as string;
|
||||
const { error } = await this.supabase.signIn(email);
|
||||
if (error) throw error;
|
||||
alert('Check your email for the login link!');
|
||||
this.loading = true
|
||||
const email = this.signInForm.value.email as string
|
||||
const { error } = await this.supabase.signIn(email)
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.signInForm.reset();
|
||||
this.loading = false;
|
||||
this.signInForm.reset()
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,10 +222,10 @@ Users also need a way to edit their profile details and manage their accounts af
|
||||
Create an **AccountComponent** with the `ng g c account` Angular CLI command.
|
||||
|
||||
```ts title="src/app/account/account.component.ts"
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { AuthSession } from '@supabase/supabase-js';
|
||||
import { Profile, SupabaseService } from '../supabase.service';
|
||||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { FormBuilder } from '@angular/forms'
|
||||
import { AuthSession } from '@supabase/supabase-js'
|
||||
import { Profile, SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
@@ -358,17 +233,17 @@ import { Profile, SupabaseService } from '../supabase.service';
|
||||
styleUrls: ['./account.component.css'],
|
||||
})
|
||||
export class AccountComponent implements OnInit {
|
||||
loading = false;
|
||||
profile!: Profile;
|
||||
loading = false
|
||||
profile!: Profile
|
||||
|
||||
@Input()
|
||||
session!: AuthSession;
|
||||
session!: AuthSession
|
||||
|
||||
updateProfileForm = this.formBuilder.group({
|
||||
username: '',
|
||||
website: '',
|
||||
avatar_url: '',
|
||||
});
|
||||
})
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
@@ -376,98 +251,90 @@ export class AccountComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.getProfile();
|
||||
await this.getProfile()
|
||||
|
||||
const { username, website, avatar_url } = this.profile;
|
||||
const { username, website, avatar_url } = this.profile
|
||||
this.updateProfileForm.patchValue({
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async getProfile() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const { user } = this.session;
|
||||
let { data: profile, error, status } = await this.supabase.profile(user);
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
let { data: profile, error, status } = await this.supabase.profile(user)
|
||||
|
||||
if (error && status !== 406) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
this.profile = profile;
|
||||
this.profile = profile
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateProfile(): Promise<void> {
|
||||
try {
|
||||
this.loading = true;
|
||||
const { user } = this.session;
|
||||
this.loading = true
|
||||
const { user } = this.session
|
||||
|
||||
const username = this.updateProfileForm.value.username as string;
|
||||
const website = this.updateProfileForm.value.website as string;
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string;
|
||||
const username = this.updateProfileForm.value.username as string
|
||||
const website = this.updateProfileForm.value.website as string
|
||||
const avatar_url = this.updateProfileForm.value.avatar_url as string
|
||||
|
||||
const { error } = await this.supabase.updateProfile({
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
});
|
||||
if (error) throw error;
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.supabase.signOut();
|
||||
await this.supabase.signOut()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```html title="src/app/account/account.component.html"
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<form
|
||||
[formGroup]="updateProfileForm"
|
||||
(ngSubmit)="updateProfile()"
|
||||
class="form-widget"
|
||||
>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" [value]="session.user.email" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input
|
||||
formControlName="username"
|
||||
id="username"
|
||||
type="text"
|
||||
/>
|
||||
<input formControlName="username" id="username" type="text" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input
|
||||
formControlName="website"
|
||||
id="website"
|
||||
type="url"
|
||||
/>
|
||||
<input formControlName="website" id="website" type="url" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="button primary block"
|
||||
[disabled]="loading"
|
||||
>
|
||||
<button type="submit" class="button primary block" [disabled]="loading">
|
||||
{{ loading ? 'Loading ...' : 'Update' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -483,8 +350,8 @@ export class AccountComponent implements OnInit {
|
||||
Now that we have all the components in place, let's update **AppComponent**:
|
||||
|
||||
```ts title="src/app/app.component.ts"
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SupabaseService } from './supabase.service';
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SupabaseService } from './supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -492,14 +359,14 @@ import { SupabaseService } from './supabase.service';
|
||||
styleUrls: ['./app.component.css'],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'angular-user-management';
|
||||
title = 'angular-user-management'
|
||||
|
||||
session = this.supabase.session;
|
||||
session = this.supabase.session
|
||||
|
||||
constructor(private readonly supabase: SupabaseService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.supabase.authChanges((_, session) => (this.session = session));
|
||||
this.supabase.authChanges((_, session) => (this.session = session))
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -516,14 +383,14 @@ export class AppComponent implements OnInit {
|
||||
`app.module.ts` also needs to be modified to include the `ReactiveFormsModule` from the `@angular/forms` package.
|
||||
|
||||
```ts title="src/app/app.module.ts"
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { AuthComponent } from './auth/auth.component';
|
||||
import { AccountComponent } from './account/account.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { AvatarComponent } from './avatar/avatar.component';
|
||||
import { AppComponent } from './app.component'
|
||||
import { AuthComponent } from './auth/auth.component'
|
||||
import { AccountComponent } from './account/account.component'
|
||||
import { ReactiveFormsModule } from '@angular/forms'
|
||||
import { AvatarComponent } from './avatar/avatar.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -559,9 +426,9 @@ Let's create an avatar for the user so that they can upload a profile photo.
|
||||
Create an **AvatarComponent** with `ng g c avatar` Angular CLI command.
|
||||
|
||||
```ts title="src/app/avatar/avatar.component.ts"
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
|
||||
import { SupabaseService } from '../supabase.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-avatar',
|
||||
@@ -569,17 +436,17 @@ import { SupabaseService } from '../supabase.service';
|
||||
styleUrls: ['./avatar.component.css'],
|
||||
})
|
||||
export class AvatarComponent implements OnInit {
|
||||
_avatarUrl: SafeResourceUrl | undefined;
|
||||
uploading = false;
|
||||
_avatarUrl: SafeResourceUrl | undefined
|
||||
uploading = false
|
||||
|
||||
@Input()
|
||||
set avatarUrl(url: string | null) {
|
||||
if (url) {
|
||||
this.downloadImage(url);
|
||||
this.downloadImage(url)
|
||||
}
|
||||
}
|
||||
|
||||
@Output() upload = new EventEmitter<string>();
|
||||
@Output() upload = new EventEmitter<string>()
|
||||
|
||||
constructor(
|
||||
private readonly supabase: SupabaseService,
|
||||
@@ -590,38 +457,38 @@ export class AvatarComponent implements OnInit {
|
||||
|
||||
async downloadImage(path: string) {
|
||||
try {
|
||||
const { data } = await this.supabase.downLoadImage(path);
|
||||
const { data } = await this.supabase.downLoadImage(path)
|
||||
if (data instanceof Blob) {
|
||||
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(
|
||||
URL.createObjectURL(data)
|
||||
);
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error('Error downloading image: ', error.message);
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAvatar(event: any) {
|
||||
try {
|
||||
this.uploading = true;
|
||||
this.uploading = true
|
||||
if (!event.target.files || event.target.files.length === 0) {
|
||||
throw new Error('You must select an image to upload.');
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const filePath = `${Math.random()}.${fileExt}`;
|
||||
const file = event.target.files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
await this.supabase.uploadAvatar(filePath, file);
|
||||
this.upload.emit(filePath);
|
||||
await this.supabase.uploadAvatar(filePath, file)
|
||||
this.upload.emit(filePath)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
this.uploading = false;
|
||||
this.uploading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,10 +529,12 @@ export class AvatarComponent implements OnInit {
|
||||
And then we can add the widget on top of the **AccountComponent** html template:
|
||||
|
||||
```html title="src/app/account.component.html"
|
||||
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
|
||||
<app-avatar
|
||||
[avatarUrl]="this.avatarUrl"
|
||||
(upload)="updateAvatar($event)">
|
||||
<form
|
||||
[formGroup]="updateProfileForm"
|
||||
(ngSubmit)="updateProfile()"
|
||||
class="form-widget"
|
||||
>
|
||||
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)">
|
||||
</app-avatar>
|
||||
<!-- input fields -->
|
||||
</form>
|
||||
@@ -682,14 +551,14 @@ And add an `updateAvatar` function along with an `avatarUrl` getter to the **Acc
|
||||
export class AccountComponent implements OnInit {
|
||||
// ...
|
||||
get avatarUrl() {
|
||||
return this.updateProfileForm.value.avatar_url as string;
|
||||
return this.updateProfileForm.value.avatar_url as string
|
||||
}
|
||||
|
||||
async updateAvatar(event: string): Promise<void> {
|
||||
this.updateProfileForm.patchValue({
|
||||
avatar_url: event,
|
||||
});
|
||||
await this.updateProfile();
|
||||
})
|
||||
await this.updateProfile()
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
@@ -7,129 +7,18 @@ sidebar_label: Expo
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
<QuickstartIntro />
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React Native. It includes:
|
||||

|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
### GitHub
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/expo-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
|
||||
@@ -7,131 +7,18 @@ sidebar_label: Flutter
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Flutter. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
<!-- - Supabase [Storage](/docs/guides/storage): users can upload a photo. -->
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supasquad/supabase-flutter-quickstart).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/flutter-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true);
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -146,12 +33,12 @@ an app called `supabase_quickstart`:
|
||||
flutter create supabase_quickstart
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [`supabase_flutter`](https://github.com/supabase/supabase-flutter)
|
||||
Then let's install the only additional dependency: [`supabase_flutter`](https://pub.dev/packages/supabase_flutter)
|
||||
|
||||
Run the following command to get the newest version of `supabase_flutter` to your project.
|
||||
Copy and paste the following line in your pubspec.yaml to install the package:
|
||||
|
||||
```bash
|
||||
flutter pub add supabase_flutter
|
||||
```yaml
|
||||
supabase_flutter: ^1.0.0
|
||||
```
|
||||
|
||||
Run `flutter pub get` to install the dependencies.
|
||||
@@ -160,12 +47,8 @@ Run `flutter pub get` to install the dependencies.
|
||||
|
||||
Now that we have the dependencies installed let's setup deep links so users who have logged in via magic link or OAuth can come back to the app.
|
||||
|
||||
```sh
|
||||
1. Go to the "Authentication" section.
|
||||
2. Click "Settings" in the sidebar.
|
||||
3. Type `io.supabase.flutterquickstart://login-callback/` in the Additional Redirect URLs input field.
|
||||
4. Hit save.
|
||||
```
|
||||
1. Go to [Authentication Settings](https://app.supabase.com/project/_/auth/settings) in the Dashboard.
|
||||
1. In **Redirect URLs**, add `io.supabase.flutterquickstart://login-callback/` as a new domain.
|
||||
|
||||

|
||||
|
||||
@@ -236,77 +119,19 @@ Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Supabase.initialize(
|
||||
url: '[YOUR_SUPABASE_URL]',
|
||||
anonKey: '[YOUR_SUPABASE_ANON_KEY]',
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
```
|
||||
|
||||
### Set up AuthState
|
||||
|
||||
In order to handle deep links for Android and iOS, let's create a class that will do just that.
|
||||
`supabase_flutter` plugin comes with `SupabaseAuthState` class where we can inherit from to react to various deep link events.
|
||||
|
||||
```dart title="lib/components/auth_state.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/utils/constants.dart';
|
||||
|
||||
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
|
||||
@override
|
||||
void onUnauthenticated() {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onAuthenticated(Session session) {
|
||||
if (mounted) {
|
||||
Navigator.of(context)
|
||||
.pushNamedAndRemoveUntil('/account', (route) => false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onPasswordRecovery(Session session) {}
|
||||
|
||||
@override
|
||||
void onErrorAuthenticating(String message) {
|
||||
context.showErrorSnackBar(message: message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Set up AuthRequiredState
|
||||
|
||||
We might want to show certain pages to the users only if they are signed in.
|
||||
For that we can create a handy `AuthRequiredState` class that we can inherit from for pages where the users need to be authenticated.
|
||||
`AuthRequiredState` will inherit `SupabaseAuthRequiredState` which is provided by `supabase_flutter` package.
|
||||
|
||||
```dart title="lib/components/auth_required_state.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class AuthRequiredState<T extends StatefulWidget>
|
||||
extends SupabaseAuthRequiredState<T> {
|
||||
@override
|
||||
void onUnauthenticated() {
|
||||
/// Users will be sent back to the LoginPage if they sign out.
|
||||
if (mounted) {
|
||||
/// Users will be sent back to the LoginPage if they sign out.
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### Setting up some constants and handy functions
|
||||
|
||||
Let's also create a constant file to make it easier to use Supabase client.
|
||||
We will also include an extension method declaration to call `showSnackBar` with one line of code.
|
||||
|
||||
```dart title="lib/utils/constants.dart"
|
||||
```dart title="lib/constants.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
@@ -332,24 +157,40 @@ extension ShowSnackBar on BuildContext {
|
||||
### Set up Splash Screen
|
||||
|
||||
Let's create a splash screen that will be shown to users right after they open the app.
|
||||
This splash screen inherits `AuthState` to redirect users to the appropriate pages depending on their authentication state.
|
||||
This screen retrieves the current session and redirects the user accordingly.
|
||||
|
||||
```dart title="lib/pages/splash_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_quickstart/components/auth_state.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({Key? key}) : super(key: key);
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
_SplashPageState createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends AuthState<SplashPage> {
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
bool _redicrectCalled = false;
|
||||
@override
|
||||
void initState() {
|
||||
recoverSupabaseSession();
|
||||
super.initState();
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_redirect();
|
||||
}
|
||||
|
||||
Future<void> _redirect() async {
|
||||
await Future.delayed(Duration.zero);
|
||||
if (_redicrectCalled || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
_redicrectCalled = true;
|
||||
final session = supabase.auth.currentSession;
|
||||
if (session != null) {
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
} else {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -359,48 +200,56 @@ class _SplashPageState extends AuthState<SplashPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Set up a Login page
|
||||
|
||||
Let's create a Flutter widget to manage logins and sign ups.
|
||||
We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
This page will also inherit `AuthState` as it will handle user login.
|
||||
Notice that this page sets up a listener on the user's auth state using `onAuthStateChange`.
|
||||
A new event will fire when the user comes back to the app by clicking their magic link, which this page can catch and redirect the user accordingly.
|
||||
|
||||
```dart title="lib/pages/login_page.dart"
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'package:supabase_quickstart/components/auth_state.dart';
|
||||
import 'package:supabase_quickstart/utils/constants.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({Key? key}) : super(key: key);
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends AuthState<LoginPage> {
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
bool _isLoading = false;
|
||||
bool _redirecting = false;
|
||||
late final TextEditingController _emailController;
|
||||
late final StreamSubscription<AuthState> _authStateSubscription;
|
||||
|
||||
Future<void> _signIn() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
final response = await supabase.auth.signIn(
|
||||
try {
|
||||
await supabase.auth.signInWithOtp(
|
||||
email: _emailController.text,
|
||||
options: AuthOptions(
|
||||
redirectTo: kIsWeb
|
||||
? null
|
||||
: 'io.supabase.flutterquickstart://login-callback/'));
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
emailRedirectTo:
|
||||
kIsWeb ? null : 'io.supabase.flutterquickstart://login-callback/',
|
||||
);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Check your email for login link!');
|
||||
_emailController.clear();
|
||||
}
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} else {
|
||||
context.showSnackBar(message: 'Check your email for login link!');
|
||||
_emailController.clear();
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occured');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -410,13 +259,22 @@ class _LoginPageState extends AuthState<LoginPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_emailController = TextEditingController();
|
||||
_authStateSubscription = supabase.auth.onAuthStateChange.listen((data) {
|
||||
if (_redirecting) return;
|
||||
final session = data.session;
|
||||
if (session != null) {
|
||||
_redirecting = true;
|
||||
Navigator.of(context).pushReplacementNamed('/account');
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_authStateSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -449,46 +307,48 @@ class _LoginPageState extends AuthState<LoginPage> {
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
Let's create a new widget called `account_page.dart` for that.
|
||||
Note that this page will inherit `AuthRequiredState` as user needs to be authenticated to view this page.
|
||||
|
||||
```dart title="lib/pages/account_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'package:supabase_quickstart/components/auth_required_state.dart';
|
||||
import 'package:supabase_quickstart/utils/constants.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/components/avatar.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({Key? key}) : super(key: key);
|
||||
const AccountPage({super.key});
|
||||
|
||||
@override
|
||||
_AccountPageState createState() => _AccountPageState();
|
||||
}
|
||||
|
||||
class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
String? _avatarUrl;
|
||||
var _loading = false;
|
||||
|
||||
/// Called once a user id is received within `onAuthenticated()`
|
||||
Future<void> _getProfile(String userId) async {
|
||||
Future<void> _getProfile() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
final response = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', userId)
|
||||
.single()
|
||||
.execute();
|
||||
final error = response.error;
|
||||
if (error != null && response.status != 406) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
}
|
||||
final data = response.data;
|
||||
if (data != null) {
|
||||
|
||||
try {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', userId)
|
||||
.single() as Map;
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected exception occured');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
@@ -508,12 +368,15 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
'website': website,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
final response = await supabase.from('profiles').upsert(updates).execute();
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} else {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpeted error occured');
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
@@ -521,19 +384,22 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
final response = await supabase.auth.signOut();
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occured');
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onAuthenticated(Session session) {
|
||||
final user = session.user;
|
||||
if (user != null) {
|
||||
_getProfile(user.id);
|
||||
}
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -561,10 +427,11 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update')),
|
||||
onPressed: _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -584,12 +451,10 @@ import 'package:supabase_quickstart/pages/login_page.dart';
|
||||
import 'package:supabase_quickstart/pages/splash_page.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Supabase.initialize(
|
||||
// TODO: Replace credentials with your own
|
||||
url: '[YOUR_SUPABASE_URL]',
|
||||
anonKey: '[YOUR_SUPABASE_ANNON_KEY]',
|
||||
url: 'YOUR_SUPABASE_URL',
|
||||
anonKey: 'YOUR_SUPABASE_ANON_KEY',
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
@@ -601,10 +466,15 @@ class MyApp extends StatelessWidget {
|
||||
title: 'Supabase Flutter',
|
||||
theme: ThemeData.dark().copyWith(
|
||||
primaryColor: Colors.green,
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.green,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
onPrimary: Colors.white,
|
||||
primary: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -650,10 +520,10 @@ You should see an orange `Public` badge next to your bucket name if your bucket
|
||||
|
||||
We will use [`image_picker`](https://pub.dev/packages/image_picker) plugin to select an image from the device.
|
||||
|
||||
Run the following command to install it.
|
||||
Add the following line in your pubspec.yaml file to install `image_picker`:
|
||||
|
||||
```bash
|
||||
flutter pub add image_picker
|
||||
```yaml
|
||||
image_picker: ^0.8.4
|
||||
```
|
||||
|
||||
Using [`image_picker`](https://pub.dev/packages/image_picker) requires some additional preparation depending on the platform.
|
||||
@@ -669,14 +539,15 @@ We can start by creating a new component:
|
||||
```dart title="lib/components/avatar.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:supabase_quickstart/utils/constants.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
|
||||
class Avatar extends StatefulWidget {
|
||||
const Avatar({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.imageUrl,
|
||||
required this.onUpload,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String? imageUrl;
|
||||
final void Function(String) onUpload;
|
||||
@@ -692,7 +563,7 @@ class _AvatarState extends State<Avatar> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.imageUrl == null)
|
||||
if (widget.imageUrl == null || widget.imageUrl!.isEmpty)
|
||||
Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
@@ -717,8 +588,8 @@ class _AvatarState extends State<Avatar> {
|
||||
}
|
||||
|
||||
Future<void> _upload() async {
|
||||
final _picker = ImagePicker();
|
||||
final imageFile = await _picker.pickImage(
|
||||
final picker = ImagePicker();
|
||||
final imageFile = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
maxWidth: 300,
|
||||
maxHeight: 300,
|
||||
@@ -728,23 +599,31 @@ class _AvatarState extends State<Avatar> {
|
||||
}
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final bytes = await imageFile.readAsBytes();
|
||||
final fileExt = imageFile.path.split('.').last;
|
||||
final fileName = '${DateTime.now().toIso8601String()}.$fileExt';
|
||||
final filePath = fileName;
|
||||
final response =
|
||||
await supabase.storage.from('avatars').uploadBinary(filePath, bytes);
|
||||
try {
|
||||
final bytes = await imageFile.readAsBytes();
|
||||
final fileExt = imageFile.path.split('.').last;
|
||||
final fileName = '${DateTime.now().toIso8601String()}.$fileExt';
|
||||
final filePath = fileName;
|
||||
await supabase.storage.from('avatars').uploadBinary(
|
||||
filePath,
|
||||
bytes,
|
||||
fileOptions: FileOptions(contentType: imageFile.mimeType),
|
||||
);
|
||||
final imageUrlResponse = await supabase.storage
|
||||
.from('avatars')
|
||||
.createSignedUrl(filePath, 60 * 60 * 24 * 365 * 10);
|
||||
widget.onUpload(imageUrlResponse);
|
||||
} on StorageException catch (error) {
|
||||
if (mounted) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
if (mounted) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occured');
|
||||
}
|
||||
}
|
||||
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
return;
|
||||
}
|
||||
final imageUrlResponse =
|
||||
supabase.storage.from('avatars').getPublicUrl(filePath);
|
||||
widget.onUpload(imageUrlResponse.data!);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -755,46 +634,45 @@ And then we can add the widget to the Account page as well as some logic to upda
|
||||
|
||||
```dart title="lib/pages/account_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'package:supabase_quickstart/components/auth_required_state.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_quickstart/components/avatar.dart';
|
||||
import 'package:supabase_quickstart/utils/constants.dart';
|
||||
import 'package:supabase_quickstart/constants.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({Key? key}) : super(key: key);
|
||||
const AccountPage({super.key});
|
||||
|
||||
@override
|
||||
_AccountPageState createState() => _AccountPageState();
|
||||
}
|
||||
|
||||
class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
class _AccountPageState extends State<AccountPage> {
|
||||
final _usernameController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
String? _userId;
|
||||
String? _avatarUrl;
|
||||
var _loading = false;
|
||||
|
||||
/// Called once a user id is received within `onAuthenticated()`
|
||||
Future<void> _getProfile(String userId) async {
|
||||
Future<void> _getProfile() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
final response = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', userId)
|
||||
.single()
|
||||
.execute();
|
||||
final error = response.error;
|
||||
if (error != null && response.status != 406) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
}
|
||||
final data = response.data;
|
||||
if (data != null) {
|
||||
|
||||
try {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
final data = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', userId)
|
||||
.single() as Map;
|
||||
_usernameController.text = (data['username'] ?? '') as String;
|
||||
_websiteController.text = (data['website'] ?? '') as String;
|
||||
_avatarUrl = (data['avatar_url'] ?? '') as String;
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected exception occured');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
@@ -814,12 +692,15 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
'website': website,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
final response = await supabase.from('profiles').upsert(updates).execute();
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
try {
|
||||
await supabase.from('profiles').upsert(updates);
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} else {
|
||||
context.showSnackBar(message: 'Successfully updated profile!');
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpeted error occured');
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
@@ -827,42 +708,47 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
final response = await supabase.auth.signOut();
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} on AuthException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error occured');
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/');
|
||||
}
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
|
||||
/// Called when image has been uploaded to Supabase storage from within Avatar widget
|
||||
Future<void> _onUpload(String imageUrl) async {
|
||||
final response = await supabase.from('profiles').upsert({
|
||||
'id': _userId,
|
||||
'avatar_url': imageUrl,
|
||||
}).execute();
|
||||
final error = response.error;
|
||||
if (error != null) {
|
||||
try {
|
||||
final userId = supabase.auth.currentUser!.id;
|
||||
await supabase.from('profiles').upsert({
|
||||
'id': userId,
|
||||
'avatar_url': imageUrl,
|
||||
});
|
||||
if (mounted) {
|
||||
context.showSnackBar(message: 'Updated your profile image!');
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
context.showErrorSnackBar(message: error.message);
|
||||
} catch (error) {
|
||||
context.showErrorSnackBar(message: 'Unexpected error has occured');
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_avatarUrl = imageUrl;
|
||||
});
|
||||
context.showSnackBar(message: 'Updated your profile image!');
|
||||
}
|
||||
|
||||
@override
|
||||
void onAuthenticated(Session session) {
|
||||
final user = session.user;
|
||||
if (user != null) {
|
||||
_userId = user.id;
|
||||
_getProfile(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onUnauthenticated() {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getProfile();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -895,10 +781,11 @@ class _AccountPageState extends AuthRequiredState<AccountPage> {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update')),
|
||||
onPressed: _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
TextButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -7,143 +7,18 @@ sidebar_label: Ionic Angular
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Ionic Angular. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started!
|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/mhartington/supabase-ionic-angular).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/mhartington/supabase-ionic-angular).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
|
||||
@@ -7,143 +7,18 @@ sidebar_label: Ionic React
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Ionic React. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started!
|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/mhartington/supabase-ionic-react).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/mhartington/supabase-ionic-react).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
|
||||
@@ -7,143 +7,18 @@ sidebar_label: Ionic Vue
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Ionic Vue. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started!
|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/mhartington/supabase-ionic-vue).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/mhartington/supabase-ionic-vue).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
|
||||
@@ -7,161 +7,18 @@ sidebar_label: 'Next.js'
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Next.js. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
<!-- ## Video demo
|
||||
### GitHub
|
||||
|
||||
Coming soon.
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management).
|
||||
|
||||
Use the annotated timeline to step through this tutorial.
|
||||
|
||||
## Instant deploy
|
||||
|
||||
Want to deploy a completed version of this example without following the guide?
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Freact-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-react-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started: -->
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
groupId="language"
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public profiles
|
||||
create table profiles (
|
||||
id uuid references auth.users not null primary key,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
-- Set up Row Level Security (RLS)
|
||||
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
|
||||
alter table profiles
|
||||
enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone." on profiles
|
||||
for select using (true);
|
||||
|
||||
create policy "Users can insert their own profile." on profiles
|
||||
for insert with check (auth.uid() = id);
|
||||
|
||||
create policy "Users can update own profile." on profiles
|
||||
for update using (auth.uid() = id);
|
||||
|
||||
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
|
||||
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.profiles (id, full_name, avatar_url)
|
||||
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
-- Set up access controls for storage.
|
||||
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
|
||||
create policy "Avatar images are publicly accessible." on storage.objects
|
||||
for select using (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can upload an avatar." on storage.objects
|
||||
for insert with check (bucket_id = 'avatars');
|
||||
|
||||
create policy "Anyone can update their own avatar." on storage.objects
|
||||
for update using ( auth.uid() = owner ) with check (bucket_id = 'avatars');
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -240,21 +97,23 @@ npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
|
||||
Wrap your `pages/_app.js` component with the `SessionContextProvider` component:
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import '../styles/globals.css'
|
||||
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { SessionContextProvider } from '@supabase/auth-helpers-react'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter()
|
||||
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
|
||||
|
||||
return (
|
||||
<SessionContextProvider
|
||||
supabaseClient={supabaseClient}
|
||||
initialSession={pageProps.initialSession}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -285,6 +144,7 @@ function MyApp({
|
||||
</SessionContextProvider>
|
||||
)
|
||||
}
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript.
|
||||
@@ -305,11 +165,10 @@ npm install @supabase/auth-ui-react
|
||||
Add the `Auth` component to your home page
|
||||
|
||||
```jsx title="pages/index.js"
|
||||
import type { NextPage } from 'next'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const Home = () => {
|
||||
const session = useSession()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
@@ -898,7 +757,9 @@ return (
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel.
|
||||
- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs).
|
||||
- Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui).
|
||||
- Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs).
|
||||
- Explore the [Supabase Cache Helpers](https://github.com/psteinroe/supabase-cache-helpers).
|
||||
- See the [Next.js Supscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) template on GitHub.
|
||||
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
|
||||
- Sign in: [app.supabase.com](https://app.supabase.com)
|
||||
@@ -7,135 +7,18 @@ sidebar_label: Nuxt 3
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Nuxt 3. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### Github
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase-community/nuxt3-quickstarter).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nuxt3-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -220,7 +103,7 @@ Let's set up a Vue component to manage logins and sign ups. We'll use Magic Link
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signIn({ email: email.value })
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
@@ -324,6 +207,7 @@ Let's create a new component for that called `Account.vue`.
|
||||
loading.value = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
user.value = null
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
|
||||
@@ -7,152 +7,18 @@ sidebar_label: React
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and React. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
<!-- ## Video demo
|
||||
### GitHub
|
||||
|
||||
Coming soon.
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/react-user-management).
|
||||
|
||||
Use the annotated timeline to step through this tutorial.
|
||||
|
||||
## Instant deploy
|
||||
|
||||
Want to deploy a completed version of this example without following the guide?
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Freact-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-react-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started: -->
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -195,7 +61,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `src/index.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/react-user-management/src/index.css).
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ export default App
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `web/src/index.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/react-user-management/src/index.css).
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css).
|
||||
|
||||
### Start RedwoodJS and your first Page
|
||||
|
||||
|
||||
@@ -7,152 +7,18 @@ sidebar_label: SolidJS
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Solid JS. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
<!-- ## Video demo
|
||||
### GitHub
|
||||
|
||||
Coming soon.
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/solid-user-management).
|
||||
|
||||
Use the annotated timeline to step through this tutorial.
|
||||
|
||||
## Instant deploy
|
||||
|
||||
Want to deploy a completed version of this example without following the guide?
|
||||
|
||||
[](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fsolid-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-solid-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management)
|
||||
|
||||
Clicking this button the application will:
|
||||
|
||||
- Launch and prepare the Postgres database in Supabase.
|
||||
- Launch the app in Vercel.
|
||||
- Fork the example into your own GitHub account.
|
||||
- Prepare the deployed application with all the necessary environment variables.
|
||||
|
||||
If you want to do it yourself, let's get started: -->
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -194,7 +60,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `src/index.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/solid-user-management/src/index.css).
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/solid-user-management/src/index.css).
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
@@ -271,9 +137,9 @@ interface Props {
|
||||
|
||||
const Account: Component<Props> = ({ session }) => {
|
||||
const [loading, setLoading] = createSignal(true)
|
||||
const [username, setUsername] = createSignal<string | null>(null)
|
||||
const [website, setWebsite] = createSignal<string | null>(null)
|
||||
const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
|
||||
const [username, setUsername] = (createSignal < string) | (null > null)
|
||||
const [website, setWebsite] = (createSignal < string) | (null > null)
|
||||
const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
|
||||
|
||||
createEffect(() => {
|
||||
getProfile()
|
||||
|
||||
@@ -7,135 +7,18 @@ sidebar_label: Svelte
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Svelte. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/svelte-user-management).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/svelte-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -178,7 +61,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
|
||||
And 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/examples/main/supabase-js-v2/user-management/svelte-user-management/src/app.css).
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
|
||||
@@ -7,135 +7,18 @@ sidebar_label: SvelteKit
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Svelte. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### Github
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/yustarandomname/svelte-supabase-quickstart).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/sveltekit-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new project to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -162,186 +45,258 @@ And finally we want to save the environment variables in a `.env`.
|
||||
All we need are the `SUPABASE_URL` and the `SUPABASE_KEY` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
```bash title=".env"
|
||||
VITE_SUPABASE_URL="YOUR_SUPABASE_URL"
|
||||
VITE_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
PUBLIC_SUPABASE_URL="YOUR_SUPABASE_URL"
|
||||
PUBLIC_SUPABASE_ANON_KEY="YOUR_SUPABASE_KEY"
|
||||
```
|
||||
|
||||
Now that we have the API credentials in place, let's create a helper file to initialize the Supabase client. These variables will be exposed
|
||||
on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```js title="lib/supabaseClient.js"
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
```js title="src/lib/supabaseClient.ts"
|
||||
import { createClient } from '@supabase/auth-helpers-sveltekit'
|
||||
import { env } from '$env/dynamic/public'
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
|
||||
```
|
||||
|
||||
And one optional step is to update the CSS file `public/global.css` to make the app look nice.
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/svelte-user-management/src/app.css).
|
||||
You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/svelte-user-management/src/app.css).
|
||||
|
||||
### Supabase Auth Helpers
|
||||
|
||||
SvelteKit is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and more.
|
||||
|
||||
It can be challenging to authenticate your users in all these different environments, that's why we've created the Supabase Auth Helpers to make user management and data fetching within SvelteKit as easy as possible.
|
||||
|
||||
Install the auth helpers for SvelteKit:
|
||||
|
||||
```bash
|
||||
npm install @supabase/auth-helpers-sveltekit
|
||||
```
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
|
||||
```html title="src/routes/+layout.svelte"
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
import { invalidate } from '$app/navigation'
|
||||
import { onMount } from 'svelte'
|
||||
import './styles.css'
|
||||
|
||||
onMount(() => {
|
||||
const {
|
||||
data: { subscription }
|
||||
} = supabaseClient.auth.onAuthStateChange(() => {
|
||||
invalidate('supabase:auth')
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0">
|
||||
<slot />
|
||||
</div>
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.ts` file to handle the session on the client-side.
|
||||
|
||||
```ts title="src/routes/+layout.ts"
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { getSupabase } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
export const load: LayoutLoad = async (event) => {
|
||||
const { session } = await getSupabase(event);
|
||||
return { session };
|
||||
};
|
||||
```
|
||||
|
||||
Create a new `src/routes/+layout.server.ts` file to handle the session on the server-side.
|
||||
|
||||
```ts title="src/routes/+layout.server.ts"
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getServerSession } from '@supabase/auth-helpers-sveltekit';
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
return {
|
||||
session: await getServerSession(event)
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Be sure to create `src/hooks.client.ts` and `src/hooks.server.ts` in order to get the auth helper started on the client and server-side.
|
||||
|
||||
```ts title="src/hooks.client.ts"
|
||||
import '$lib/supabaseClient'
|
||||
```
|
||||
|
||||
```ts title="src/hooks.server.ts"
|
||||
import '$lib/supabaseClient'
|
||||
```
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
Let's set up a Svelte component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
|
||||
|
||||
```html title="lib/Auth.svelte"
|
||||
<script>
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
```html title="src/routes/Auth.svelte"
|
||||
<script lang="ts">
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
|
||||
let loading = false
|
||||
let email
|
||||
let loading = false
|
||||
let email: string
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabase.auth.signIn({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
alert(error.error_description || error.message)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { error } = await supabaseClient.auth.signInWithOtp({ email })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="row flex-center flex" on:submit|preventDefault="{handleLogin}">
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + Svelte</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<div>
|
||||
<input
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
bind:value="{email}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class='button block' value={loading ? "Loading" :
|
||||
"Send magic link"} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<form class="row flex-center flex" on:submit|preventDefault={handleLogin}>
|
||||
<div class="col-6 form-widget">
|
||||
<h1 class="header">Supabase + SvelteKit</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<div>
|
||||
<input class="inputField" type="email" placeholder="Your email" bind:value={email} />
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block"
|
||||
value={loading ? 'Loading' : 'Send magic link'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
### User store
|
||||
### Account component
|
||||
|
||||
To access the user information in other places, we use a writable store. Create a new file called `sessionStore.js`
|
||||
After a user is signed in, they need to be able to edit their profile details and manage their account.
|
||||
Create a new `Account.svelte` component to handle this functionality.
|
||||
|
||||
```javascript title="lib/sessionStore.js"
|
||||
import { writable } from 'svelte/store'
|
||||
```html title="src/routes/Account.svelte"
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { AuthSession } from '@supabase/supabase-js'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
|
||||
export const user = writable(false)
|
||||
```
|
||||
export let session: AuthSession
|
||||
|
||||
### Account page
|
||||
let loading = false
|
||||
let username: string | null = null
|
||||
let website: string | null = null
|
||||
let avatarUrl: string | null = null
|
||||
|
||||
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 `Profile.svelte`.
|
||||
onMount(() => {
|
||||
getProfile()
|
||||
})
|
||||
|
||||
```html title="lib/Profile.svelte"
|
||||
<script>
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import { user } from '$lib/sessionStore'
|
||||
const getProfile = async () => {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
let loading = true
|
||||
let username = null
|
||||
let website = null
|
||||
let avatar_url = null
|
||||
const { data, error, status } = await supabaseClient
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
function getProfile(node) {
|
||||
try {
|
||||
loading = true
|
||||
const user = supabase.auth.user()
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatarUrl = data.avatar_url
|
||||
}
|
||||
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user?.id)
|
||||
.single()
|
||||
.then(({ data, error, status }) => {
|
||||
if (data) {
|
||||
username = data.username
|
||||
website = data.website
|
||||
avatar_url = data.avatar_url
|
||||
}
|
||||
if (error && status !== 406) throw error
|
||||
})
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
if (error && status !== 406) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading = true
|
||||
const user = supabase.auth.user()
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading = true
|
||||
const { user } = session
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username,
|
||||
website,
|
||||
avatar_url: avatarUrl,
|
||||
updated_at: new Date()
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates, {
|
||||
returning: 'minimal', // Don't return the value after inserting
|
||||
})
|
||||
let { error } = await supabaseClient.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
loading = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
async function signOut() {
|
||||
try {
|
||||
loading = true
|
||||
let { error } = await supabaseClient.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
use:getProfile
|
||||
class="form-widget"
|
||||
on:submit|preventDefault="{updateProfile}"
|
||||
>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value="{$user.email}" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input id="username" type="text" bind:value="{username}" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" type="website" bind:value="{website}" />
|
||||
</div>
|
||||
<form class="form-widget" on:submit|preventDefault={updateProfile}>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="text" value={session.user.email} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Name</label>
|
||||
<input id="username" type="text" bind:value={username} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="website">Website</label>
|
||||
<input id="website" type="website" bind:value={website} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" class="button block primary" value={loading ? 'Loading
|
||||
...' : 'Update'} disabled={loading}/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block primary"
|
||||
value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="button block" on:click="{signOut}" disabled="{loading}">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="button block" on:click={signOut} disabled={loading}> Sign Out </button>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
@@ -351,25 +306,21 @@ Now that we have all the components in place, let's update `src/routes/+page.sve
|
||||
|
||||
```html title="src/routes/+page.svelte"
|
||||
<script>
|
||||
import { user } from '$lib/sessionStore'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
import Auth from '$lib/Auth.svelte'
|
||||
import Profile from '$lib/Profile.svelte'
|
||||
|
||||
user.set(supabase.auth.user())
|
||||
|
||||
supabase.auth.onAuthStateChange((_, session) => {
|
||||
user.set(session.user)
|
||||
})
|
||||
import { page } from '$app/stores'
|
||||
import Account from './Account.svelte'
|
||||
import Auth from './Auth.svelte'
|
||||
</script>
|
||||
|
||||
<div class="container" style="padding: 50px 0 100px 0;">
|
||||
{#if $user}
|
||||
<Profile />
|
||||
{:else}
|
||||
<Auth />
|
||||
{/if}
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>Supabase + SvelteKit</title>
|
||||
<meta name="description" content="SvelteKit using supabase-js v2" />
|
||||
</svelte:head>
|
||||
|
||||
{#if !$page.data.session}
|
||||
<Auth />
|
||||
{:else}
|
||||
<Account session={$page.data.session} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window:
|
||||
@@ -390,89 +341,95 @@ Every Supabase project is configured with [Storage](/docs/guides/storage) for ma
|
||||
|
||||
Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component:
|
||||
|
||||
```html title="lib/Avatar.svelte"
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabase } from '$lib/supabaseClient'
|
||||
```html title="src/routes/Avatar.svelte"
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { supabaseClient } from '$lib/supabaseClient'
|
||||
|
||||
export let path
|
||||
export let size = '10em'
|
||||
export let size = 10
|
||||
export let url: string
|
||||
|
||||
let uploading = false
|
||||
let src
|
||||
let files
|
||||
let avatarUrl: string | null = null
|
||||
let uploading = false
|
||||
let files: FileList
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function downloadImage() {
|
||||
supabase.storage
|
||||
.from('avatars')
|
||||
.download(path)
|
||||
.then(({ data, error }) => {
|
||||
if (error) throw error
|
||||
src = URL.createObjectURL(data)
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error('Error downloading image: ', error.message)
|
||||
)
|
||||
}
|
||||
const downloadImage = async (path: string) => {
|
||||
try {
|
||||
const { data, error } = await supabaseClient.storage.from('avatars').download(path)
|
||||
|
||||
async function uploadAvatar() {
|
||||
try {
|
||||
uploading = true
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
const url = URL.createObjectURL(data)
|
||||
avatarUrl = url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.log('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${Math.random()}.${fileExt}`
|
||||
const filePath = `${fileName}`
|
||||
const uploadAvatar = async () => {
|
||||
try {
|
||||
uploading = true
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
if (!files || files.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
if (uploadError) throw uploadError
|
||||
const file = files[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
path = filePath
|
||||
dispatch('upload')
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
let { error } = await supabaseClient.storage.from('avatars').upload(filePath, file)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
url = filePath
|
||||
dispatch('upload')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
uploading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: if (url) downloadImage(url)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if path}
|
||||
<img
|
||||
use:downloadImage
|
||||
{src}
|
||||
alt="Avatar"
|
||||
class="avatar image"
|
||||
style="height: {size}; width: {size};"
|
||||
/>
|
||||
{:else}
|
||||
<div class="avatar no-image" style="height: {size}; width: {size};" />
|
||||
{/if}
|
||||
{#if avatarUrl}
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={avatarUrl ? 'Avatar' : 'No image'}
|
||||
class="avatar image"
|
||||
style="height: {size}em; width: {size}em;"
|
||||
/>
|
||||
{:else}
|
||||
<div class="avatar no-image" style="height: {size}em; width: {size}em;" />
|
||||
{/if}
|
||||
|
||||
<div style="width: {size};">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position:absolute;"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change="{uploadAvatar}"
|
||||
disabled="{uploading}"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: {size}em;">
|
||||
<label class="button primary block" for="single">
|
||||
{uploading ? 'Uploading ...' : 'Upload'}
|
||||
</label>
|
||||
<input
|
||||
style="visibility: hidden; position:absolute;"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
bind:files
|
||||
on:change={uploadAvatar}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -480,10 +437,10 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
And then we can add the widget to the Account page:
|
||||
|
||||
```html title="$lib/Profile.svelte"
|
||||
```html title="src/routes/Account.svelte"
|
||||
<script>
|
||||
// Import the new component
|
||||
import Avatar from '$lib/Avatar.svelte'
|
||||
import Avatar from './Avatar.svelte'
|
||||
</script>
|
||||
|
||||
<form
|
||||
@@ -492,7 +449,7 @@ And then we can add the widget to the Account page:
|
||||
on:submit|preventDefault="{updateProfile}"
|
||||
>
|
||||
<!-- Add to body -->
|
||||
<Avatar bind:path="{avatar_url}" on:upload="{updateProfile}" />
|
||||
<Avatar bind:url={avatarUrl} size={10} on:upload={updateProfile} />
|
||||
|
||||
<!-- Other form elements -->
|
||||
</form>
|
||||
|
||||
@@ -7,135 +7,18 @@ sidebar_label: 'Vue 3'
|
||||
|
||||
import Tabs from '@theme/Tabs'
|
||||
import TabItem from '@theme/TabItem'
|
||||
import QuickstartIntro from './_partials/quickstart_intro.md'
|
||||
import ProjectSetup from './_partials/project_setup.mdx'
|
||||
|
||||
## Intro
|
||||
|
||||
This example provides the steps to build a simple user management app (from scratch!) using Supabase and Vue 3. It includes:
|
||||
|
||||
- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data.
|
||||
- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email).
|
||||
- Supabase [Storage](/docs/guides/storage): users can upload a photo.
|
||||
- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data.
|
||||
- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables.
|
||||
|
||||
By the end of this guide you'll have an app which allows users to login and update some basic profile details:
|
||||
<QuickstartIntro />
|
||||
|
||||

|
||||
|
||||
### GitHub
|
||||
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/supabase/examples/tree/main/supabase-js-v2/user-management/vue3-user-management).
|
||||
Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/vue3-user-management).
|
||||
|
||||
## Project set up
|
||||
|
||||
Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase
|
||||
and then creating a "schema" inside the database.
|
||||
|
||||
### Create a project
|
||||
|
||||
1. Go to [app.supabase.com](https://app.supabase.com).
|
||||
1. Click on "New Project".
|
||||
1. Enter your project details.
|
||||
1. Wait for the new database to launch.
|
||||
|
||||
### Set up the database schema
|
||||
|
||||
Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor,
|
||||
or you can just copy/paste the SQL from below and run it yourself.
|
||||
|
||||
<Tabs
|
||||
defaultValue="dashboard"
|
||||
values={[
|
||||
{label: 'Dashboard', value: 'dashboard'},
|
||||
{label: 'SQL', value: 'sql'},
|
||||
]}>
|
||||
<TabItem value="dashboard">
|
||||
|
||||
1. Go to the [SQL Editor](https://app.supabase.com/project/_/sql) page in the Dashboard.
|
||||
2. Click **User Management Starter**.
|
||||
3. Click **Run**.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/sql-user-management-starter.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sql">
|
||||
|
||||
```sql
|
||||
-- Create a table for public "profiles"
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
using ( true );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime!
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage!
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Get the API Keys
|
||||
|
||||
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
|
||||
We just need to get the URL and `anon` key from the API settings.
|
||||
|
||||
1. Go to the [Settings](https://app.supabase.com/project/_/settings) page in the Dashboard.
|
||||
2. Click **API** in the sidebar.
|
||||
3. Find your API `URL`, `anon`, and `service_role` keys on this page.
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source
|
||||
src="/docs/videos/api/api-url-and-key.mp4"
|
||||
type="video/mp4"
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</video>
|
||||
<ProjectSetup />
|
||||
|
||||
## Building the App
|
||||
|
||||
@@ -182,7 +65,7 @@ const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
```
|
||||
|
||||
Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/examples/main/supabase-js-v2/user-management/vue3-user-management/src/style.css) to style the app.
|
||||
Optionally, update [src/style.css](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/vue3-user-management/src/style.css) to style the app.
|
||||
|
||||
### Set up a Login component
|
||||
|
||||
@@ -190,26 +73,28 @@ Let's set up a Vue component to manage logins and sign ups. We'll use Magic Link
|
||||
|
||||
```html title="/src/components/Auth.vue"
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
import { ref } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
const email = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithOtp({ email: email.value })
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email: email.value,
|
||||
})
|
||||
if (error) throw error
|
||||
alert('Check your email for the login link!')
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -218,11 +103,20 @@ const handleLogin = async () => {
|
||||
<h1 class="header">Supabase + Vue 3</h1>
|
||||
<p class="description">Sign in via magic link with your email below</p>
|
||||
<div>
|
||||
<input class="inputField" type="email" placeholder="Your email" v-model="email" />
|
||||
<input
|
||||
class="inputField"
|
||||
type="email"
|
||||
placeholder="Your email"
|
||||
v-model="email"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="button block" :value="loading ? 'Loading' : 'Send magic link'"
|
||||
:disabled="loading" />
|
||||
<input
|
||||
type="submit"
|
||||
class="button block"
|
||||
:value="loading ? 'Loading' : 'Send magic link'"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -236,80 +130,80 @@ Let's create a new component for that called `Account.vue`.
|
||||
|
||||
```html title="src/components/Account.vue"
|
||||
<script setup>
|
||||
import { supabase } from '../supabase'
|
||||
import { onMounted, ref, toRefs } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
import { onMounted, ref, toRefs } from 'vue'
|
||||
|
||||
const props = defineProps(['session'])
|
||||
const { session } = toRefs(props)
|
||||
const props = defineProps(['session'])
|
||||
const { session } = toRefs(props)
|
||||
|
||||
const loading = ref(true)
|
||||
const username = ref('')
|
||||
const website = ref('')
|
||||
const avatar_url = ref('')
|
||||
const loading = ref(true)
|
||||
const username = ref('')
|
||||
const website = ref('')
|
||||
const avatar_url = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
getProfile()
|
||||
})
|
||||
onMounted(() => {
|
||||
getProfile()
|
||||
})
|
||||
|
||||
async function getProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
async function getProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
let { data, error, status } = await supabase
|
||||
.from('profiles')
|
||||
.select(`username, website, avatar_url`)
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
if (error && status !== 406) throw error
|
||||
if (error && status !== 406) throw error
|
||||
|
||||
if (data) {
|
||||
username.value = data.username
|
||||
website.value = data.website
|
||||
avatar_url.value = data.avatar_url
|
||||
if (data) {
|
||||
username.value = data.username
|
||||
website.value = data.website
|
||||
avatar_url.value = data.avatar_url
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
async function updateProfile() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { user } = session.value
|
||||
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username: username.value,
|
||||
website: website.value,
|
||||
avatar_url: avatar_url.value,
|
||||
updated_at: new Date(),
|
||||
const updates = {
|
||||
id: user.id,
|
||||
username: username.value,
|
||||
website: website.value,
|
||||
avatar_url: avatar_url.value,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profiles').upsert(updates)
|
||||
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function signOut() {
|
||||
try {
|
||||
loading.value = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
async function signOut() {
|
||||
try {
|
||||
loading.value = true
|
||||
let { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -328,8 +222,12 @@ async function signOut() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" class="button primary block" :value="loading ? 'Loading ...' : 'Update'"
|
||||
:disabled="loading" />
|
||||
<input
|
||||
type="submit"
|
||||
class="button primary block"
|
||||
:value="loading ? 'Loading ...' : 'Update'"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -347,22 +245,22 @@ Now that we have all the components in place, let's update `App.vue`:
|
||||
|
||||
```html title="src/App.vue"
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Account from './components/Account.vue'
|
||||
import Auth from './components/Auth.vue'
|
||||
import { supabase } from './supabase'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Account from './components/Account.vue'
|
||||
import Auth from './components/Auth.vue'
|
||||
import { supabase } from './supabase'
|
||||
|
||||
const session = ref()
|
||||
const session = ref()
|
||||
|
||||
onMounted(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
session.value = data.session
|
||||
onMounted(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
session.value = data.session
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_, _session) => {
|
||||
session.value = _session
|
||||
})
|
||||
})
|
||||
|
||||
supabase.auth.onAuthStateChange((_, _session) => {
|
||||
session.value = _session
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -393,72 +291,87 @@ Let's create an avatar for the user so that they can upload a profile photo. We
|
||||
|
||||
```html title="src/components/Avatar.vue"
|
||||
<script setup>
|
||||
import { ref, toRefs, watch } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
import { ref, toRefs, watch } from 'vue'
|
||||
import { supabase } from '../supabase'
|
||||
|
||||
const prop = defineProps(['path', 'size'])
|
||||
const { path, size } = toRefs(prop)
|
||||
const prop = defineProps(['path', 'size'])
|
||||
const { path, size } = toRefs(prop)
|
||||
|
||||
const emit = defineEmits(['upload', 'update:path'])
|
||||
const uploading = ref(false)
|
||||
const src = ref('')
|
||||
const files = ref()
|
||||
const emit = defineEmits(['upload', 'update:path'])
|
||||
const uploading = ref(false)
|
||||
const src = ref('')
|
||||
const files = ref()
|
||||
|
||||
const downloadImage = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path.value)
|
||||
if (error) throw error
|
||||
src.value = URL.createObjectURL(data)
|
||||
} catch (error) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadAvatar = async (evt) => {
|
||||
files.value = evt.target.files
|
||||
try {
|
||||
uploading.value = true
|
||||
if (!files.value || files.value.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
const downloadImage = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('avatars')
|
||||
.download(path.value)
|
||||
if (error) throw error
|
||||
src.value = URL.createObjectURL(data)
|
||||
} catch (error) {
|
||||
console.error('Error downloading image: ', error.message)
|
||||
}
|
||||
|
||||
const file = files.value[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
|
||||
if (uploadError) throw uploadError
|
||||
emit('update:path', filePath)
|
||||
emit('upload')
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(path, () => {
|
||||
if (path.value) downloadImage()
|
||||
})
|
||||
const uploadAvatar = async (evt) => {
|
||||
files.value = evt.target.files
|
||||
try {
|
||||
uploading.value = true
|
||||
if (!files.value || files.value.length === 0) {
|
||||
throw new Error('You must select an image to upload.')
|
||||
}
|
||||
|
||||
const file = files.value[0]
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const filePath = `${Math.random()}.${fileExt}`
|
||||
|
||||
let { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(filePath, file)
|
||||
|
||||
if (uploadError) throw uploadError
|
||||
emit('update:path', filePath)
|
||||
emit('upload')
|
||||
} catch (error) {
|
||||
alert(error.message)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(path, () => {
|
||||
if (path.value) downloadImage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<img v-if="src" :src="src" alt="Avatar" class="avatar image"
|
||||
:style="{ height: size + 'em', width: size + 'em' }" />
|
||||
<div v-else class="avatar no-image" :style="{ height: size + 'em', width: size + 'em' }" />
|
||||
<img
|
||||
v-if="src"
|
||||
:src="src"
|
||||
alt="Avatar"
|
||||
class="avatar image"
|
||||
:style="{ height: size + 'em', width: size + 'em' }"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="avatar no-image"
|
||||
:style="{ height: size + 'em', width: size + 'em' }"
|
||||
/>
|
||||
|
||||
<div :style="{ width: size + 'em' }">
|
||||
<label class="button primary block" for="single">
|
||||
{{ uploading ? "Uploading ..." : "Upload" }}
|
||||
</label>
|
||||
<input style="visibility: hidden; position: absolute" type="file" id="single" accept="image/*"
|
||||
@change="uploadAvatar" :disabled="uploading" />
|
||||
<input
|
||||
style="visibility: hidden; position: absolute"
|
||||
type="file"
|
||||
id="single"
|
||||
accept="image/*"
|
||||
@change="uploadAvatar"
|
||||
:disabled="uploading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -470,8 +383,8 @@ And then we can add the widget to the Account page:
|
||||
|
||||
```html title="src/components/Account.vue"
|
||||
<script>
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.vue'
|
||||
// Import the new component
|
||||
import Avatar from './Avatar.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -26,7 +26,7 @@ Reference documentation for the official Supabase client libraries, APIs, and to
|
||||
</div>
|
||||
{/* DART */}
|
||||
<div class="col col--6">
|
||||
<ButtonCard icon={useBaseUrl("/img/icons/dart-icon.svg")} class="card" to={useBaseUrl("/reference/dart")} style={{ height: '100%' }} title="Dart" description="Dart and Flutter documentation."/>
|
||||
<ButtonCard icon={useBaseUrl("/img/icons/flutter-icon.svg")} class="card" to={useBaseUrl("/reference/dart")} style={{ height: '100%' }} title="Flutter" description="Flutter and Dart documentation."/>
|
||||
</div>
|
||||
{/* CLI */}
|
||||
<div class="col col--6">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const lightCodeTheme = require('@kiwicopple/prism-react-renderer/themes/vsDark')
|
||||
const darkCodeTheme = require('@kiwicopple/prism-react-renderer/themes/vsDark')
|
||||
const mainNavbar = require('./nav/_referenceNavbar')
|
||||
const mdxMermaid = require('mdx-mermaid')
|
||||
|
||||
const baseUrl = '/docs/'
|
||||
|
||||
@@ -90,11 +91,10 @@ const config = {
|
||||
breadcrumbs: false,
|
||||
editUrl:
|
||||
'https://github.com/supabase/supabase/edit/master/apps/reference/',
|
||||
lastVersion: 'v0',
|
||||
lastVersion: 'current',
|
||||
versions: {
|
||||
current: {
|
||||
label: 'v1-dev',
|
||||
path: '/next',
|
||||
label: 'v1',
|
||||
},
|
||||
v0: {
|
||||
label: 'v0',
|
||||
@@ -136,6 +136,7 @@ const config = {
|
||||
breadcrumbs: false,
|
||||
editUrl:
|
||||
'https://github.com/supabase/supabase/edit/master/apps/reference',
|
||||
//remarkPlugins: [mdxMermaid.default],
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
|
||||
@@ -4,7 +4,7 @@ const sidebars = {
|
||||
type: 'category',
|
||||
label: 'Overview',
|
||||
collapsed: true,
|
||||
items: ['about', 'architecture', 'guides/examples'],
|
||||
items: ['about', 'guides/features', 'architecture'],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
@@ -34,7 +34,7 @@ const sidebars = {
|
||||
items: [
|
||||
'guides/cli',
|
||||
'guides/cli/local-development',
|
||||
'guides/cli/cicd-workflow',
|
||||
'guides/cli/managing-environments',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -80,6 +80,8 @@ const sidebars = {
|
||||
'guides/auth/row-level-security',
|
||||
'guides/auth/managing-user-data',
|
||||
'guides/auth/auth-captcha',
|
||||
'guides/auth/server-side-rendering',
|
||||
'guides/auth/auth-mfa',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -133,9 +135,9 @@ const sidebars = {
|
||||
collapsed: true,
|
||||
items: [
|
||||
'guides/database/extensions',
|
||||
// 'guides/database/extensions/pgtap',
|
||||
'guides/database/extensions/plv8',
|
||||
'guides/database/extensions/http',
|
||||
'guides/database/extensions/pgtap',
|
||||
'guides/database/extensions/uuid-ossp',
|
||||
],
|
||||
},
|
||||
@@ -185,6 +187,7 @@ const sidebars = {
|
||||
collapsed: true,
|
||||
items: [
|
||||
'guides/hosting/platform',
|
||||
'guides/platform/custom-domains',
|
||||
'guides/platform/disk-usage',
|
||||
'guides/platform/logs',
|
||||
'guides/platform/metrics',
|
||||
@@ -224,6 +227,7 @@ const sidebars = {
|
||||
'guides/integrations/auth0',
|
||||
'guides/integrations/authsignal',
|
||||
'guides/integrations/clerk',
|
||||
'guides/integrations/keyri',
|
||||
'guides/integrations/stytch',
|
||||
'guides/integrations/supertokens',
|
||||
],
|
||||
@@ -277,7 +281,7 @@ const sidebars = {
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Supabase Dart Library',
|
||||
label: 'Supabase Flutter Library',
|
||||
href: '/reference/dart',
|
||||
},
|
||||
{ type: 'link', label: 'Supabase CLI', href: '/reference/cli' },
|
||||
|
||||
@@ -6,7 +6,7 @@ const sidebars = {
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Getting Started',
|
||||
items: ['intro', 'installing', 'initializing'],
|
||||
items: ['intro', 'installing', 'initializing', 'upgrade-guide'],
|
||||
collapsed: true,
|
||||
},
|
||||
{
|
||||
@@ -14,12 +14,16 @@ const sidebars = {
|
||||
label: 'Auth',
|
||||
items: [
|
||||
'generated/auth-signup',
|
||||
'generated/auth-signin',
|
||||
'generated/auth-signinwithprovider',
|
||||
'generated/auth-signinwithpassword',
|
||||
'generated/auth-signinwithotp',
|
||||
'generated/auth-signinwithoauth',
|
||||
'generated/auth-signout',
|
||||
'generated/auth-update',
|
||||
'generated/auth-verifyotp',
|
||||
'generated/auth-currentsession',
|
||||
'generated/auth-currentuser',
|
||||
'generated/auth-updateuser',
|
||||
'generated/auth-onauthstatechange',
|
||||
'generated/reset-password-email',
|
||||
'generated/auth-resetpasswordforemail',
|
||||
],
|
||||
collapsed: true,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ const sidebars = {
|
||||
'generated/initializing',
|
||||
'typescript-support',
|
||||
'release-notes',
|
||||
'upgrade-guide',
|
||||
],
|
||||
collapsed: false,
|
||||
},
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"docusaurus-plugin-sass": "^0.2.2",
|
||||
"docusaurus-theme-search-typesense": "^0.6.0-0 ",
|
||||
"jsrsasign": "^10.5.26",
|
||||
"mdx-mermaid": "^1.3.2",
|
||||
"mermaid": "^9.1.7",
|
||||
"octokit": "^2.0.5",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -34,8 +34,8 @@ const headerNames = {
|
||||
icon: 'javascript-icon',
|
||||
},
|
||||
dart: {
|
||||
name: 'Dart',
|
||||
icon: 'dart-icon',
|
||||
name: 'Flutter',
|
||||
icon: 'flutter-icon',
|
||||
},
|
||||
'auth-helpers': {
|
||||
name: 'Auth Helpers',
|
||||
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg width="182" height="182" viewBox="0 0 182 182" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1306_4326)">
|
||||
<path d="M42.7001 118.9L14.8 91.0004L105.4 0.400391H161.2" fill="#44D1FD"/>
|
||||
<path d="M161.2 84H105.4L84.5 104.9L112.4 132.8" fill="#44D1FD"/>
|
||||
<path d="M84.5 160.701L105.4 181.601H161.2L112.4 132.801" fill="#08589C"/>
|
||||
<path d="M84.5214 104.957L56.6616 132.816L84.5214 160.676L112.381 132.816L84.5214 104.957Z" fill="#1FBCFD"/>
|
||||
<path d="M112.381 132.816L84.5212 160.676L88.4103 164.565L116.27 136.705L112.381 132.816Z" fill="url(#paint0_linear_1306_4326)"/>
|
||||
<path d="M84.5 160.701L125.9 146.401L112.4 132.801" fill="url(#paint1_linear_1306_4326)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1306_4326" x1="84.5212" y1="160.676" x2="88.4103" y2="164.565" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.15"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1306_4326" x1="84.5" y1="132.801" x2="125.9" y2="132.801" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.26"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1306_4326">
|
||||
<rect width="182" height="182" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg width="182" height="182" viewBox="0 0 182 182" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1306_4326)">
|
||||
<path d="M42.7001 118.9L14.8 91.0004L105.4 0.400391H161.2" fill="#44D1FD"/>
|
||||
<path d="M161.2 84H105.4L84.5 104.9L112.4 132.8" fill="#44D1FD"/>
|
||||
<path d="M84.5 160.701L105.4 181.601H161.2L112.4 132.801" fill="#08589C"/>
|
||||
<path d="M84.5214 104.957L56.6616 132.816L84.5214 160.676L112.381 132.816L84.5214 104.957Z" fill="#1FBCFD"/>
|
||||
<path d="M112.381 132.816L84.5212 160.676L88.4103 164.565L116.27 136.705L112.381 132.816Z" fill="url(#paint0_linear_1306_4326)"/>
|
||||
<path d="M84.5 160.701L125.9 146.401L112.4 132.801" fill="url(#paint1_linear_1306_4326)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1306_4326" x1="84.5212" y1="160.676" x2="88.4103" y2="164.565" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.15"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1306_4326" x1="84.5" y1="132.801" x2="125.9" y2="132.801" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.26"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1306_4326">
|
||||
<rect width="182" height="182" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -50,7 +50,7 @@ Each system has a number of configuration options which can be found in the rele
|
||||
|
||||
- [Postgres](https://hub.docker.com/_/postgres/)
|
||||
- [PostgREST](https://postgrest.org/en/stable/configuration.html)
|
||||
- [Realtime](https://github.com/supabase/realtime#server-set-up)
|
||||
- [Realtime](https://github.com/supabase/realtime#server)
|
||||
- [GoTrue](https://github.com/supabase/gotrue)
|
||||
- [Storage](https://github.com/supabase/storage-api)
|
||||
- [Kong](https://docs.konghq.com/install/docker/)
|
||||
|
||||
@@ -43,7 +43,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dart (Flutter)</td>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-dart" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
|
||||
@@ -43,7 +43,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dart (Flutter)</td>
|
||||
<td>Flutter</td>
|
||||
<td><a href="https://github.com/supabase/supabase-dart" target="_blank" rel="noopener noreferrer">supabase-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-lang</a></td>
|
||||
|
||||
@@ -95,7 +95,7 @@ Thanks to a comunity contribution ([@\_mateomorris](https://twitter.com/_mateomo
|
||||
- Ionic Integration by [Simon Grimm](https://www.youtube.com/user/saimon1924) [[Video](https://www.youtube.com/watch?v=pl9XfIWutKE)]
|
||||
- Flutter Integration by [Aditya Thakur](https://www.youtube.com/channel/UChCAJNpMwoEUYCsE_eSyU4w) by [[Video](https://www.youtube.com/watch?v=fqfHEZvQPlY)]
|
||||
- Svelte Integration by [Khaerunnisa Isnaeni](https://www.youtube.com/channel/UCqNDj6eQeTLHuEwgEHWOGjw) [[Video](https://www.youtube.com/watch?v=odPYzJJyEJI)]
|
||||
- An [example app](https://github.com/supabase/examples/tree/main/supabase-js-v1/user-management) for adding User Profiles to your Next.js app
|
||||
- An [example app](https://github.com/supabase/supabase/tree/master/examples/user-management) for adding User Profiles to your Next.js app
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ Our observability pipeline has been built [using Logflare](https://supabase.com/
|
||||

|
||||
new —> deploy —> invoke —> logs
|
||||
|
||||
To round off the end-to-end experience, `supabase-js` now works in Deno, making it easy to interact with the rest of your Supabase project from within your Edge Functions. Your project's URLs, API keys, and database connection strings are made available as environment variables within your Function to make this even easier. And if you want to interact with third-party APIs which require a secret key, such as Stripe, you can easily and securely make these available to your Function as environment variables via the Supabase CLI. To get an idea for what is possible, check out our examples page [here](https://github.com/supabase/examples/tree/main/supabase-js-v1/edge-functions).
|
||||
To round off the end-to-end experience, `supabase-js` now works in Deno, making it easy to interact with the rest of your Supabase project from within your Edge Functions. Your project's URLs, API keys, and database connection strings are made available as environment variables within your Function to make this even easier. And if you want to interact with third-party APIs which require a secret key, such as Stripe, you can easily and securely make these available to your Function as environment variables via the Supabase CLI. To get an idea for what is possible, check out our examples page [here](https://github.com/supabase/supabase/tree/master/examples/edge-functions).
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ Here's a collection of resources that will help you get started building with Su
|
||||
- [Edge Functions Guide](https://supabase.com/docs/guides/functions)
|
||||
- [Edge Functions Quickstart video](https://youtu.be/rzglqRdZUQE)
|
||||
- [Supabase CLI local development guide](https://supabase.com/docs/guides/local-development)
|
||||
- [Edge Functions examples](https://github.com/supabase/examples/tree/main/supabase-js-v1/edge-functions)
|
||||
- [Edge Functions examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions)
|
||||
|
||||
### Additional Info
|
||||
|
||||
|
||||
@@ -99,6 +99,6 @@ If you're inspired to build, check out some of the latest resources:
|
||||
- [Edge Functions Guide](https://supabase.com/docs/guides/functions)
|
||||
- [Edge Functions Quickstart video](https://youtu.be/rzglqRdZUQE)
|
||||
- [Supabase CLI local development guide](https://supabase.com/docs/guides/local-development)
|
||||
- [Edge Functions examples](https://github.com/supabase/examples/tree/main/supabase-js-v1/edge-functions)
|
||||
- [Edge Functions examples](https://github.com/supabase/supabase/tree/master/examples/edge-functions)
|
||||
|
||||
[^1]: Updated on June 14 2022 for search optimization: Removed links as account does not exist anymore on GitHub
|
||||
@@ -20,9 +20,9 @@ Wildcard redirects are especially useful for Jamstack platforms with preview dep
|
||||
|
||||
## Supabase Edge functions with Webhooks
|
||||
|
||||
Now you can deploy functions with optional JWT verification. This makes it easier to trigger Edge Functions with webhooks. Use the -no-verify-jwt flag when deploying your functions via the CLI to enable this mode. You can test this out with our [Edge Functions Telegram Bot example](https://github.com/supabase/examples/tree/main/supabase-js-v1/edge-functions/supabase/functions/telegram-bot).
|
||||
Now you can deploy functions with optional JWT verification. This makes it easier to trigger Edge Functions with webhooks. Use the -no-verify-jwt flag when deploying your functions via the CLI to enable this mode. You can test this out with our [Edge Functions Telegram Bot example](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/telegram-bot).
|
||||
|
||||
[](https://github.com/supabase/examples/tree/main/supabase-js-v1/edge-functions/supabase/functions/telegram-bot)
|
||||
[](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/telegram-bot)
|
||||
|
||||
## Metrics for everybody
|
||||
|
||||
|
||||
@@ -732,7 +732,7 @@ As usual, Postgres has the tools to solve the problem _reasonably_ well (if not
|
||||
[pg-gin]: https://www.postgresql.org/docs/14/indexes-types.html#INDEXES-TYPES-GIN
|
||||
[wiki-muri]: https://en.wikipedia.org/wiki/Muri_(Japanese_term)
|
||||
[lz4]: https://github.com/lz4/lz4
|
||||
[clickhouse]: clickhouse.tech/
|
||||
[clickhouse]: https://clickhouse.com
|
||||
[wiki-bloom-filter]: https://en.wikipedia.org/wiki/Bloom_filter
|
||||
[wiki-hll]: https://en.wikipedia.org/wiki/HyperLogLog
|
||||
[pg-cron]: https://github.com/citusdata/pg_cron
|
||||
|
||||
@@ -183,7 +183,7 @@ If you're already working towards the same goals, reach out and let's do it toge
|
||||
Launch Week 5 is starting Monday 15th August, we will launch one new feature every day for a week. If you want to follow along check out [supabase.com/launch-week](https://supabase.com/launch-week).
|
||||
|
||||
- [Launch Week 5 Hackathon](https://supabase.com/blog/launch-week-5-hackathon)
|
||||
- [DAy 1 - Supabase CLI v1 and Admin API Beta](https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta)
|
||||
- [Day 1 - Supabase CLI v1 and Admin API Beta](https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta)
|
||||
- [Youtube video - Supabase CLI v1 and Admin API Beta](https://www.youtube.com/watch?v=OpPOaJI_Z28&feature=emb_title)
|
||||
- [Day 2 - supabase-js v2 Release Candidate](https://supabase.com/blog/supabase-js-v2)
|
||||
- [Youtube Video - supabase-js v2 Release Candidate](https://www.youtube.com/watch?v=iqZlPtl_b-I)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'supabase-js v2 Release Candidate'
|
||||
description: First peek of supabase-js v2, which focuses on quality-of-life improvements for developers.
|
||||
title: 'supabase-js v2'
|
||||
description: A look at supabase-js v2, which brings type support and focuses on quality-of-life improvements for developers.
|
||||
author: inian,alaister
|
||||
image: lw5-supabase-js/thumb.jpg
|
||||
thumb: lw5-supabase-js/thumb.jpg
|
||||
@@ -10,6 +10,13 @@ date: '2022-08-16'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
<div className="bg-gray-300 rounded-lg p-6 italic">
|
||||
<br /> ⚠️ UPDATED 20/10: supabase-js v2 is fully released 🥳<br />
|
||||
|
||||
<a href="https://supabase.com/docs/reference/javascript">Check the updated docs</a> and <a href="https://supabase.com/docs/reference/javascript/upgrade-guide">migration guide</a>.
|
||||
|
||||
</div>
|
||||
|
||||
Today we're publishing a release candidate for [supabase-js v2](https://github.com/supabase/supabase-js), which focuses on “quality-of-life” improvements for developers.
|
||||
|
||||
Try it out by running `npm i @supabase/supabase-js@rc`
|
||||
@@ -299,10 +306,10 @@ Special shout outs to: [@vejja](https://github.com/vejja), [@pixtron](https://gi
|
||||
Update today by running:
|
||||
|
||||
```bash
|
||||
npm i @supabase/supabase-js@rc
|
||||
npm i @supabase/supabase-js@2
|
||||
```
|
||||
|
||||
You can find a full list of updates, breaking changes, and a migration guide in our [Release Notes](https://supabase.com/docs/reference/javascript/release-notes).
|
||||
[Migration guide](https://supabase.com/docs/reference/javascript/upgrade-guide)
|
||||
|
||||
We'll continuing merging security fixes to v1, with maintenance patches for the next three months.
|
||||
|
||||
@@ -319,18 +326,9 @@ We'll continuing merging security fixes to v1, with maintenance patches for the
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
## Links
|
||||
## supabase-js v2 resources
|
||||
|
||||
- [v2 Release Candidate Documentation](https://supabase.com/docs/reference/javascript)
|
||||
- [Release Notes & Migration Guide](https://supabase.com/docs/reference/javascript/release-notes)
|
||||
|
||||
## More Launch Week 5
|
||||
|
||||
- [Launch Week Page](https://supabase.com/launch-week)
|
||||
- [Launch Week 5 Hackathon](https://supabase.com/blog/launch-week-5-hackathon)
|
||||
- [Supabase Series B](https://supabase.com/blog/supabase-series-b)
|
||||
- [Open Source at Supabase - Founders Fireside Chat](https://www.youtube.com/watch?v=4t_63HT3rZY)
|
||||
- [Day 1 - Supabase CLI v1 and Admin API Beta](https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta)
|
||||
- [Youtube video - Supabase CLI v1 and Admin API Beta](https://www.youtube.com/watch?v=OpPOaJI_Z28&feature=emb_title)
|
||||
- [Day 3 - Supabase is SOC2 compliant](https://supabase.com/blog/supabase-soc2)
|
||||
- [Youtube video - Security Day](https://www.youtube.com/watch?v=6bGQotxisoY)
|
||||
- [v2 Documentation](https://supabase.com/docs/reference/javascript)
|
||||
- [Migration guide](https://supabase.com/docs/reference/javascript/upgrade-guide)
|
||||
- [Next.js quickstart guide](/docs/guides/with-nextjs)
|
||||
- [Examples](https://github.com/supabase/supabase/tree/master/examples)
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: 'supabase-js v2 Released'
|
||||
description: We've released supabase-js v2. Updated examples, quickstarts, and an improved experience.
|
||||
author: thor_schaeff
|
||||
image: supabase-js-v2-release/supabase-js.jpg
|
||||
thumb: supabase-js-v2-release/supabase-js.jpg
|
||||
tags:
|
||||
- launch-week
|
||||
date: '2022-10-20'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
During our [last Launch Week](/launch-week) we presented the [release candidate for supabase-js v2](/blog/supabase-js-v2). Since then we've been busy incorporating your feedback, [updating the docs](/docs/reference/javascript/), [examples](https://github.com/supabase/supabase/tree/master/examples), [quickstart guides](/docs/guides/with-nextjs), and putting a [migration guide](/docs/reference/javascript/upgrade-guide) together.
|
||||
|
||||
## What is new in v2?
|
||||
|
||||
Enhanced TypeScript support built right in was the big one! Now you can use the CLI to generate types, directly from your database.
|
||||
|
||||
Plus v2 comes with lots of improvements that solved some of the largest pain-points highlighted by our users. Your feedback help us improve: you speak, we listen.
|
||||
|
||||
Read all the updates and the differences with v1 on the [announcement blog post](/blog/supabase-js-v2)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
It truly takes a village, well, in this case an entire community, and we want to thank you all for your feedback, and contributions!
|
||||
|
||||
Version 2.0 is the result of the combined work of several Supatroopers (Alaister, Andrew, Inian, Joel, Jon, Kang, Bobbie, and Tyler), over 100 contributors to our libraries, and over 450 contributors to our docs and websites.
|
||||
|
||||
If you're one of those contributors, thank you!
|
||||
|
||||
- [`functions-js`](https://github.com/supabase/functions-js/graphs/contributors) (4)
|
||||
- [`gotrue-js`](https://github.com/supabase/gotrue-js/graphs/contributors) (47)
|
||||
- [`postgrest-js`](https://github.com/supabase/postgrest-js/graphs/contributors) (30)
|
||||
- [`realtime-js`](https://github.com/supabase/realtime-js/graphs/contributors) (16)
|
||||
- [`storage-js`](https://github.com/supabase/storage-js/graphs/contributors) (17)
|
||||
- [`supabase-js`](https://github.com/supabase/supabase-js/graphs/contributors) (39)
|
||||
|
||||
Special shout outs to: [@vejja](https://github.com/vejja), [@pixtron](https://github.com/pixtron), [@bnjmnt4n](https://github.com/pixtron), and [@karlseguin](https://github.com/karlseguin).
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](/docs/reference/javascript)
|
||||
- [Migration Guide](/docs/reference/javascript/upgrade-guide)
|
||||
- [Quickstart Guides](/docs/guides/with-nextjs)
|
||||
- [Examples (GitHub)](https://github.com/supabase/supabase/tree/master/examples)
|
||||
- [Release Notes](/docs/reference/javascript/release-notes)
|
||||
- [Release Candidate Blogpost](/blog/supabase-js-v2)
|
||||
- [Auth Helpers](/docs/guides/auth/auth-helpers/)
|
||||
- [Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui)
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: 'supabase-flutter v1 Released'
|
||||
description: We've released supabase-flutter v1. More intuitive way of accessing Supabase from your Flutter application.
|
||||
author: tyler_shukert
|
||||
image: flutter-v1-release/flutter_v1_official_release.jpeg
|
||||
thumb: flutter-v1-release/flutter_v1_official_release.jpeg
|
||||
tags:
|
||||
- flutter
|
||||
- mobile
|
||||
date: '2022-10-21'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
A few months ago, we announced a [developer preview version of supabase-flutter SDK](https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview). Since then, we have heard a lot of amazing feedback from the community, and have been improving it. Today, we are happy to announce the stable v1 of [supabase-flutter](https://pub.dev/packages/supabase_flutter). You can also find the updated [quick start guide](https://supabase.com/docs/guides/with-flutter), [documentation](https://supabase.com/docs/reference/dart) and a [migration guide from v0](https://supabase.com/docs/reference/dart/upgrade-guide).
|
||||
|
||||
## What is new in v1?
|
||||
|
||||
supabase-flutter v1 focuses on improved developer experience. The new version requires far less boiler plate code as well as it provides more intuitive APIs. Here are some highlights of the update.
|
||||
|
||||
### No more `.execute()`
|
||||
|
||||
Previously, for `.select()`, `.insert()`, `.update()`, `.delete()` and `.stream()` required `execute()` to be called at the end. On top of that, errors are thrown, not returned, so you can be sure that you have the query results in the returned value.
|
||||
|
||||
```dart
|
||||
// Before
|
||||
final response = await supabase.from('messages').select().execute();
|
||||
final data = response.data;
|
||||
|
||||
// After
|
||||
final data = await supabase.from('messages').select();
|
||||
```
|
||||
|
||||
### More predictable auth methods
|
||||
|
||||
Names of the auth methods are more descriptive about what they do. Here are some examples of the new methods:
|
||||
|
||||
```dart
|
||||
await supabase.auth.signInWithPassword(email: email, password: password);
|
||||
|
||||
await supabase.auth.signInWithOAuth(Provider.github)
|
||||
```
|
||||
|
||||
Also, `onAuthStateChange` returns stream, which feels more natural for anyone coding in Dart.
|
||||
|
||||
```dart
|
||||
supabase.auth.onAuthStateChange.listen((data) {
|
||||
final AuthChangeEvent event = data.event;
|
||||
final Session? session = data.session;
|
||||
});
|
||||
```
|
||||
|
||||
### Realtime Multiplayer edition support
|
||||
|
||||
During the last launch week, we announced the [general availability of Realtime Multiplayer](https://supabase.com/blog/supabase-realtime-multiplayer-general-availability). supabase-flutter now has first class support for the two newly introduced realtime methods, broadcast and presence. Broadcast can be used to share realtime data to all connected clients with low latency. Presence is a way to let other connected clients know the status of the client. You can visit [multiplayer.dev](http://multiplayer.dev) to see a quick demo of the feature.
|
||||
|
||||
```dart
|
||||
final channel = Supabase.instance.client.channel('my_channel');
|
||||
|
||||
// listen to `location` broadcast events
|
||||
channel.on(
|
||||
RealtimeListenTypes.broadcast,
|
||||
ChannelFilter(
|
||||
event: 'location',
|
||||
), (payload, [ref]) {
|
||||
// Do something exciting with the broadcast event
|
||||
});
|
||||
|
||||
// send `location` broadcast events
|
||||
channel.send(
|
||||
type: RealtimeListenTypes.broadcast,
|
||||
event: 'location',
|
||||
payload: {'lat': 1.3521, 'lng': 103.8198},
|
||||
);
|
||||
|
||||
// listen to presence states
|
||||
channel.on(RealtimeListenTypes.presence, ChannelFilter(event: 'sync'),
|
||||
(payload, [ref]) {
|
||||
// Do something exciting with the presence state
|
||||
});
|
||||
|
||||
// subscribe to the above changes
|
||||
channel.subscribe((status) async {
|
||||
if (status == 'SUBSCRIBED') {
|
||||
// if subscribed successfully, send presence event
|
||||
final status = await channel.track({'user_id': myUserId});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
These are just tip of the iceberg of all the updates that we shipped in v1. Check out the [documentation](https://supabase.com/docs/reference/dart/) to see the full list.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
It required massive support from the community to bring the supabase-flutter to where it is today. I would like to thank everyone who has contributed to the library, and a special thanks to [Bruno](https://github.com/bdlukaa) and [Vinzent](https://github.com/Vinzent03), who have been key for this release. We really could not have done it without you!
|
||||
|
||||
## Resources
|
||||
|
||||
- [Install supabase-flutter v1.0](https://pub.dev/packages/supabase_flutter)
|
||||
- [supabase-flutter documentation](https://supabase.com/docs/reference/dart/)
|
||||
- [v0 to v1 migration guide](https://supabase.com/docs/reference/dart/upgrade-guide)
|
||||
- [Flutter Tutorial: building a Flutter chat app](https://supabase.com/blog/flutter-tutorial-building-a-chat-app)
|
||||
- [Build a Flutter app with Very Good CLI and Supabase](https://verygood.ventures/blog/flutter-app-very-good-cli-supabase)
|
||||
@@ -21,8 +21,8 @@ const BuiltExamples = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/supabase/examples"
|
||||
as="https://github.com/supabase/examples"
|
||||
href="https://github.com/supabase/supabase/tree/master/examples"
|
||||
as="https://github.com/supabase/supabase/tree/master/examples"
|
||||
passHref
|
||||
>
|
||||
<Button as="a" type="default" icon={<IconGitHub />} size="small">
|
||||
|
||||
@@ -42,8 +42,8 @@ function GithubExamples() {
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/supabase/examples"
|
||||
as="https://github.com/supabase/examples"
|
||||
href="https://github.com/supabase/supabase/tree/master/examples"
|
||||
as="https://github.com/supabase/supabase/tree/master/examples"
|
||||
>
|
||||
<Button size="small" as="a" type="default" icon={<IconGitHub size={12} />}>
|
||||
Official GitHub library
|
||||
|
||||
@@ -30,7 +30,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'GitHub',
|
||||
stat: '38,000+',
|
||||
stat: '40,000+',
|
||||
statLabel: 'GitHub stars',
|
||||
img: 'github.png',
|
||||
invertImgDarkMode: true,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{
|
||||
"text": "Guides",
|
||||
"description": "Examples and references for using Supabase.",
|
||||
"url": "/docs/guides/examples",
|
||||
"url": "/docs/",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"author_url": "https://github.com/supabase",
|
||||
"author_img": "https://avatars.githubusercontent.com/u/54469796",
|
||||
"repo_name": "nextjs-slack-clone",
|
||||
"repo_url": "https://github.com/supabase/examples/tree/main/supabase-js-v1/slack-clone/nextjs-slack-clone",
|
||||
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone",
|
||||
"vercel_deploy_url": "",
|
||||
"demo_url": ""
|
||||
},
|
||||
@@ -138,7 +138,7 @@
|
||||
"author_url": "https://github.com/supabase",
|
||||
"author_img": "https://avatars.githubusercontent.com/u/54469796",
|
||||
"repo_name": "sveltejs-todo-list",
|
||||
"repo_url": "https://github.com/supabase/examples/tree/main/supabase-js-v1/todo-list/sveltejs-todo-list",
|
||||
"repo_url": "https://github.com/supabase/supabase/tree/master/examples/todo-list/sveltejs-todo-list",
|
||||
"vercel_deploy_url": "",
|
||||
"demo_url": ""
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"text": "Guides",
|
||||
"url": "/docs/guides/examples"
|
||||
"url": "/docs/"
|
||||
},
|
||||
{
|
||||
"text": "Changelog",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{
|
||||
"imgUrl": "images/slack-clone.jpg",
|
||||
"title": "Chat app with Next.js",
|
||||
"url": "https://github.com/supabase/examples/tree/main/supabase-js-v1/slack-clone/nextjs-slack-clone",
|
||||
"url": "https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone",
|
||||
"description": "Build a full-stack Slack clone using Next.js and Supabase.",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -1475,4 +1475,64 @@ module.exports = [
|
||||
source: '/docs/reference/javascript/getsubscriptions',
|
||||
destination: '/docs/reference/javascript/v1/getsubscriptions',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/cli/cicd-workflows',
|
||||
destination: '/docs/guides/cli/managing-environments',
|
||||
},
|
||||
|
||||
// supabase-flutter v0 to v1 redirects
|
||||
// v0: /auth-update
|
||||
// v1: /auth-updateuser
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/auth-update',
|
||||
destination: '/docs/reference/dart/auth-updateuser',
|
||||
},
|
||||
// v0: /auth-api-resetpasswordforemail
|
||||
// v1: /auth-resetpasswordforemail
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/reset-password-email',
|
||||
destination: '/docs/reference/dart/auth-resetpasswordforemail',
|
||||
},
|
||||
// signIn method is now split into signInWithPassword ,signInWithPasswordless ,signInWithOAuth
|
||||
// send traffic to v0 docs instead
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/auth-signin',
|
||||
destination: '/docs/reference/dart/v0/auth-signin',
|
||||
},
|
||||
// v0: /auth-session
|
||||
// v1: /auth-currentsession
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/auth-session',
|
||||
destination: '/docs/reference/dart/auth-currentsession',
|
||||
},
|
||||
// v0: /auth-user
|
||||
// v1: /auth-currentuser
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/auth-user',
|
||||
destination: '/docs/reference/dart/auth-currentuser',
|
||||
},
|
||||
// v0: /auth-signinwithprovider
|
||||
// v1: /auth-signinwithoauth
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/v0/auth-signinwithprovider',
|
||||
destination: '/docs/reference/dart/auth-signinwithoauth',
|
||||
},
|
||||
// realtime methods been replaced with new names
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/removesubscription',
|
||||
destination: '/docs/reference/dart/v0/removesubscription',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/reference/dart/getsubscriptions',
|
||||
destination: '/docs/reference/dart/v0/getsubscriptions',
|
||||
},
|
||||
]
|
||||
@@ -52,7 +52,7 @@
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.2",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/marked": "^4.0.3",
|
||||
@@ -64,9 +64,9 @@
|
||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"file-loader": "^6.2.0",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.2.1",
|
||||
"tailwindcss": "^3.1.8"
|
||||
|
||||
@@ -155,7 +155,7 @@ function StoragePage() {
|
||||
author_img={'https://avatars.githubusercontent.com/u/54469796'}
|
||||
repo_name={'nextjs-ts-user-management'}
|
||||
repo_url={
|
||||
'https://github.com/supabase/examples/tree/main/supabase-js-v1/user-management/nextjs-ts-user-management'
|
||||
'https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management'
|
||||
}
|
||||
vercel_deploy_url={
|
||||
'https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-mangement%2Fnextjs-ts-user-management&project-name=supabase-user-management&repository-name=supabase-user-management&demo-title=Supabase%20User%20Management&demo-description=An%20example%20web%20app%20using%20Supabase%20and%20Next.js&demo-url=https%3A%2F%2Fsupabase-nextjs-ts-user-management.vercel.app&demo-image=https%3A%2F%2Fi.imgur.com%2FZ3HkQqe.png&integration-ids=oac_jUduyjQgOyzev1fjrW83NYOv&external-id=nextjs-user-management'
|
||||
|
||||
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 626 KiB |
@@ -1 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 34 42" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><style><![CDATA[.B{fill-rule:nonzero}]]></style><use xlink:href="#C" x=".5" y=".5"/><defs><linearGradient id="A" x1="19.68%" y1="119.30%" x2="69.89%" y2="44.82%"><stop offset="0%" stop-color="#1a237e" stop-opacity=".4"/><stop offset="1%" stop-color="#1a237e" stop-opacity="0"/></linearGradient><radialGradient id="B" cx="3%" cy="7%" r="135%" fx="3%" fy="7%"><stop offset="0%" stop-color="#fff" stop-opacity=".1"/><stop offset="1%" stop-color="#fff" stop-opacity="0"/></radialGradient></defs><symbol id="C" overflow="visible"><g stroke="none"><path d="M15.504 35.667l17.057-17.061H20.159L9.305 29.464zM0 20.159L20.159 0h12.403L6.203 26.362z" fill="#39cefd" class="B"/><path d="M20.158 40.318h12.403L21.707 29.464l-6.203 6.203z" fill="#03569b" class="B"/><path d="M21.707 29.464l-6.203 6.203 9.2-3.187z" fill="url(#A)" class="B"/><path d="M9.304 29.461l6.203-6.203 6.204 6.203-6.204 6.203z" fill="#16b9fd" class="B"/><path d="M32.561 18.606L21.708 29.464l10.854 10.858H20.159L9.301 29.464l10.858-10.858h12.403zM20.159 0L0 20.159l6.203 6.203L32.561 0H20.159z" fill="url(#B)" class="B"/></g></symbol></svg>
|
||||
<svg width="182" height="182" viewBox="0 0 182 182" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1306_4326)">
|
||||
<path d="M42.7001 118.9L14.8 91.0004L105.4 0.400391H161.2" fill="#44D1FD"/>
|
||||
<path d="M161.2 84H105.4L84.5 104.9L112.4 132.8" fill="#44D1FD"/>
|
||||
<path d="M84.5 160.701L105.4 181.601H161.2L112.4 132.801" fill="#08589C"/>
|
||||
<path d="M84.5214 104.957L56.6616 132.816L84.5214 160.676L112.381 132.816L84.5214 104.957Z" fill="#1FBCFD"/>
|
||||
<path d="M112.381 132.816L84.5212 160.676L88.4103 164.565L116.27 136.705L112.381 132.816Z" fill="url(#paint0_linear_1306_4326)"/>
|
||||
<path d="M84.5 160.701L125.9 146.401L112.4 132.801" fill="url(#paint1_linear_1306_4326)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1306_4326" x1="84.5212" y1="160.676" x2="88.4103" y2="164.565" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.15"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1306_4326" x1="84.5" y1="132.801" x2="125.9" y2="132.801" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2" stop-opacity="0.26"/>
|
||||
<stop offset="0.85" stop-color="#616161" stop-opacity="0.01"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1306_4326">
|
||||
<rect width="182" height="182" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -45,7 +45,7 @@ services:
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.10.0
|
||||
image: supabase/gotrue:v2.19.4
|
||||
depends_on:
|
||||
- db # Disable this if you are using an external Postgres database
|
||||
restart: unless-stopped
|
||||
@@ -98,7 +98,7 @@ services:
|
||||
|
||||
realtime:
|
||||
container_name: supabase-realtime
|
||||
image: supabase/realtime:v0.22.4
|
||||
image: supabase/realtime:v0.25.1
|
||||
depends_on:
|
||||
- db # Disable this if you are using an external Postgres database
|
||||
restart: unless-stopped
|
||||
@@ -161,8 +161,13 @@ services:
|
||||
# Comment out everything below this point if you are using an external Postgres database
|
||||
db:
|
||||
container_name: supabase-db
|
||||
image: supabase/postgres:14.1.0.21
|
||||
command: postgres -c config_file=/etc/postgresql/postgresql.conf
|
||||
image: supabase/postgres:14.1.0.77
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- config_file=/etc/postgresql/postgresql.conf
|
||||
- -c
|
||||
- log_min_messages=fatal # prevents Realtime polling queries from appearing in logs
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Supabase Examples Archive
|
||||
|
||||
## supabase-js v1
|
||||
|
||||
You can find the supabase-js v1 examples at [github.com/supabase/examples-archive](https://github.com/supabase/examples-archive).
|
||||
@@ -1,3 +0,0 @@
|
||||
dist/*
|
||||
|
||||
.vercel
|
||||
@@ -1,14 +0,0 @@
|
||||
# Vanilla-js Auth Example
|
||||
|
||||
Live Example: [https://auth-vanilla-js.vercel.app/](https://auth-vanilla-js.vercel.app/)
|
||||
|
||||
How to sign up and login using supabase and supabase-js using HTML and JavaScript only
|
||||
|
||||
<img width="558" alt="image" src="https://user-images.githubusercontent.com/458736/88377414-b6fb4180-cdd1-11ea-8061-103ec4577b7b.png">
|
||||
|
||||
### running
|
||||
|
||||
`npm run dev` (requires npx to be installed)
|
||||
|
||||
if you want to make changes without restarting the server run this in a different terminal window:
|
||||
`npm run watch`
|
||||
@@ -1,16 +0,0 @@
|
||||
body {
|
||||
/* margin: 0; */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: rgb(250, 217, 246);
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 1em;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@1"></script>
|
||||
<script src="./index.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='section'>
|
||||
<h1>Supabase Auth Example</h1>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<a href="https://github.com/supabase/supabase/tree/master/examples/auth/javascript-auth">View the code on GitHub</a>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='sign-up'>
|
||||
<h3>Sign Up</h3>
|
||||
<label>Email</label><input type='email' name='email' />
|
||||
<label>Password</label><input type='password' name='password' />
|
||||
<input type='submit'>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='log-in'>
|
||||
<h3>Log In</h3>
|
||||
<label>Email</label><input type='email' name='email' />
|
||||
<label>Password</label><input type='password' name='password' />
|
||||
<input type='submit'>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<form id='validate'>
|
||||
<h3>Access Token</h3>
|
||||
<input readonly=readonly type='text' id='access-token' /> <small>Default expiry is 60 minutes</small>
|
||||
<h3>Refresh Token</h3>
|
||||
<input readonly=readonly type='text' id='refresh-token' /> <small>Supabase-js will use this to automatically fetch a new accessToken for you every 60 mins whilst the client is running</small>
|
||||
</form>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<h3>Fetch User Details</h3>
|
||||
<button id='user-button'>Fetch</button>
|
||||
</div>
|
||||
<div class='section'>
|
||||
<h3>Logout</h3>
|
||||
<button id='logout-button'>Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,79 +0,0 @@
|
||||
var SUPABASE_URL = 'https://ernhobnpmmupjnmxpfbt.supabase.co'
|
||||
var SUPABASE_KEY =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzA5ODU0NCwiZXhwIjoxOTI4Njc0NTQ0fQ.Z9bRrfaL2oGhSuyBckFcdcnRIJDelWJ1II98OnEtLO0'
|
||||
|
||||
var supabase = supabase.createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
window.userToken = null
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function (event) {
|
||||
var signUpForm = document.querySelector('#sign-up')
|
||||
signUpForm.onsubmit = signUpSubmitted.bind(signUpForm)
|
||||
|
||||
var logInForm = document.querySelector('#log-in')
|
||||
logInForm.onsubmit = logInSubmitted.bind(logInForm)
|
||||
|
||||
var userDetailsButton = document.querySelector('#user-button')
|
||||
userDetailsButton.onclick = fetchUserDetails.bind(userDetailsButton)
|
||||
|
||||
var logoutButton = document.querySelector('#logout-button')
|
||||
logoutButton.onclick = logoutSubmitted.bind(logoutButton)
|
||||
})
|
||||
|
||||
const signUpSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
const email = event.target[0].value
|
||||
const password = event.target[1].value
|
||||
|
||||
supabase.auth
|
||||
.signUp({ email, password })
|
||||
.then((response) => {
|
||||
response.error ? alert(response.error.message) : setToken(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const logInSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
const email = event.target[0].value
|
||||
const password = event.target[1].value
|
||||
|
||||
supabase.auth
|
||||
.signIn({ email, password })
|
||||
.then((response) => {
|
||||
response.error ? alert(response.error.message) : setToken(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.response.text)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchUserDetails = () => {
|
||||
alert(JSON.stringify(supabase.auth.user()))
|
||||
}
|
||||
|
||||
const logoutSubmitted = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
supabase.auth
|
||||
.signOut()
|
||||
.then((_response) => {
|
||||
document.querySelector('#access-token').value = ''
|
||||
document.querySelector('#refresh-token').value = ''
|
||||
alert('Logout successful')
|
||||
})
|
||||
.catch((err) => {
|
||||
alert(err.response.text)
|
||||
})
|
||||
}
|
||||
|
||||
function setToken(response) {
|
||||
if (response.user.confirmation_sent_at && !response?.session?.access_token) {
|
||||
alert('Confirmation Email Sent')
|
||||
} else {
|
||||
document.querySelector('#access-token').value = response.session.access_token
|
||||
document.querySelector('#refresh-token').value = response.session.refresh_token
|
||||
alert('Logged in as ' + response.user.email)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "supabase-auth-vanilla-js",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "supabase-auth-vanilla-js",
|
||||
"version": "0.0.1",
|
||||
"description": "basic auth example for supabase with js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "npx light-server -s ."
|
||||
},
|
||||
"author": "ant@supabase.io",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,3 +0,0 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
@@ -1,35 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/
|
||||
.env
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||