mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Merge branch 'master' into chore/update-og-launch-week-2
This commit is contained in:
commit
3d9cbab5aa
24 files changed
+949
-97
No files matched your search
@@ -41,7 +41,7 @@ services:
|
||||
|
||||
rest:
|
||||
container_name: supabase-rest
|
||||
image: postgrest/postgrest:latest
|
||||
image: postgrest/postgrest:nightly-2021-03-05-19-03-d3a8b5f
|
||||
ports:
|
||||
- ${REST_PORT}:3000
|
||||
depends_on:
|
||||
@@ -49,9 +49,10 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/postgres
|
||||
PGRST_DB_SCHEMA: public
|
||||
PGRST_DB_SCHEMA: public, storage
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
realtime:
|
||||
container_name: supabase-realtime
|
||||
image: supabase/realtime:latest
|
||||
@@ -71,6 +72,30 @@ services:
|
||||
# Disable JWT Auth locally. The JWT_SECRET will be ignored.
|
||||
SECURE_CHANNELS: 'false'
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
storage:
|
||||
image: supabase/storage-api:v0.8.3
|
||||
ports:
|
||||
- '5000:5000'
|
||||
depends_on:
|
||||
- db
|
||||
- rest
|
||||
restart: on-failure
|
||||
environment:
|
||||
ANON_KEY: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoxNjkwNDQzMTM1LCJhdWQiOiIiLCJzdWIiOiIiLCJSb2xlIjoiYW5vbiIsImp0aSI6ImQ1N2ZkMjFlLTNkYjQtNDk3YS1hNGViLTI4ZDY3Yzg2NjEwMiJ9.YySz_BoYxYco0b2aP3iBMt-dVUqC6RpnQiSb4Zt-L-o
|
||||
SERVICE_KEY: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoxNjkwNDQzMTM1LCJhdWQiOiIiLCJyb2xlIjoic2VydmljZV9yb2xlIiwianRpIjoiOTk4MzJiYmItNDdhNS00NzJhLTgxMDQtYzY4ZGVjZDQyZDVlIn0._DToQdxDcSLsp3tPpacsxswv0p1KRti0QOTmnbfs5JY
|
||||
PROJECT_REF: bjwdssmqcnupljrqypxz # can be any random string
|
||||
POSTGREST_URL: http://rest:3000
|
||||
PGRST_JWT_SECRET: ${JWT_SECRET}
|
||||
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/postgres
|
||||
PGOPTIONS: "-c search_path=storage"
|
||||
FILE_SIZE_LIMIT: 52428800
|
||||
REGION: us-east-1 # region where your bucket is located
|
||||
GLOBAL_S3_BUCKET: supa-storage-testing # name of s3 bucket where you want to store objects
|
||||
# AWS_ACCESS_KEY_ID: replace-with-your-aws-key
|
||||
# AWS_SECRET_ACCESS_KEY: replace-with-your-aws-secret
|
||||
STORAGE_BACKEND: file
|
||||
FILE_STORAGE_BACKEND_PATH: /var/data/storage
|
||||
db:
|
||||
container_name: supabase-db
|
||||
build:
|
||||
@@ -85,4 +110,4 @@ services:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
@@ -39,6 +39,16 @@ services:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage-api:5000/*'
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
consumers:
|
||||
- username: 'anon-key'
|
||||
keyauth_credentials:
|
||||
|
||||
@@ -2,6 +2,7 @@ FROM supabase/postgres:0.13.0
|
||||
|
||||
COPY 00-initial-schema.sql /docker-entrypoint-initdb.d/00-initial-schema.sql
|
||||
COPY auth-schema.sql /docker-entrypoint-initdb.d/auth-schema.sql
|
||||
COPY storage-schema.sql /docker-entrypoint-initdb.d/storage-schema.sql
|
||||
|
||||
# Build time defaults
|
||||
ARG build_POSTGRES_DB=postgres
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
CREATE SCHEMA IF NOT EXISTS storage AUTHORIZATION postgres;
|
||||
|
||||
grant usage on schema storage to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema storage grant all on tables to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema storage grant all on functions to postgres, anon, authenticated, service_role;
|
||||
alter default privileges in schema storage grant all on sequences to postgres, anon, authenticated, service_role;
|
||||
|
||||
DROP TABLE IF EXISTS "storage"."buckets";
|
||||
CREATE TABLE "storage"."buckets" (
|
||||
"id" text not NULL,
|
||||
"name" text NOT NULL,
|
||||
"owner" uuid,
|
||||
"created_at" timestamptz DEFAULT now(),
|
||||
"updated_at" timestamptz DEFAULT now(),
|
||||
CONSTRAINT "buckets_owner_fkey" FOREIGN KEY ("owner") REFERENCES "auth"."users"("id"),
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "bname" ON "storage"."buckets" USING BTREE ("name");
|
||||
|
||||
DROP TABLE IF EXISTS "storage"."objects";
|
||||
CREATE TABLE "storage"."objects" (
|
||||
"id" uuid NOT NULL DEFAULT extensions.uuid_generate_v4(),
|
||||
"bucket_id" text,
|
||||
"name" text,
|
||||
"owner" uuid,
|
||||
"created_at" timestamptz DEFAULT now(),
|
||||
"updated_at" timestamptz DEFAULT now(),
|
||||
"last_accessed_at" timestamptz DEFAULT now(),
|
||||
"metadata" jsonb,
|
||||
CONSTRAINT "objects_bucketId_fkey" FOREIGN KEY ("bucket_id") REFERENCES "storage"."buckets"("id"),
|
||||
CONSTRAINT "objects_owner_fkey" FOREIGN KEY ("owner") REFERENCES "auth"."users"("id"),
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE UNIQUE INDEX "bucketid_objname" ON "storage"."objects" USING BTREE ("bucket_id","name");
|
||||
CREATE INDEX name_prefix_search ON storage.objects(name text_pattern_ops);
|
||||
|
||||
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE OR REPLACE FUNCTION storage.foldername(name text)
|
||||
RETURNS text[]
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
return _parts[1:array_length(_parts,1)-1];
|
||||
END
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION storage.filename(name text)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
return _parts[array_length(_parts,1)];
|
||||
END
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION storage.extension(name text)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
_parts text[];
|
||||
_filename text;
|
||||
BEGIN
|
||||
select string_to_array(name, '/') into _parts;
|
||||
select _parts[array_length(_parts,1)] into _filename;
|
||||
return split_part(_filename, '.', 2);
|
||||
END
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION storage.search(prefix text, bucketname text, limits int DEFAULT 100, levels int DEFAULT 1, offsets int DEFAULT 0)
|
||||
RETURNS TABLE (
|
||||
name text,
|
||||
id uuid,
|
||||
updated_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ,
|
||||
last_accessed_at TIMESTAMPTZ,
|
||||
metadata jsonb
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
_bucketId text;
|
||||
BEGIN
|
||||
select buckets."id" from buckets where buckets.name=bucketname limit 1 into _bucketId;
|
||||
return query
|
||||
with files_folders as (
|
||||
select ((string_to_array(objects.name, '/'))[levels]) as folder
|
||||
from objects
|
||||
where objects.name ilike prefix || '%'
|
||||
and bucket_id = _bucketId
|
||||
GROUP by folder
|
||||
limit limits
|
||||
offset offsets
|
||||
)
|
||||
select files_folders.folder as name, objects.id, objects.updated_at, objects.created_at, objects.last_accessed_at, objects.metadata from files_folders
|
||||
left join objects
|
||||
on prefix || files_folders.folder = objects.name
|
||||
where objects.id is null or objects.bucket_id=_bucketId;
|
||||
END
|
||||
$function$;
|
||||
|
||||
GRANT ALL PRIVILEGES ON SCHEMA storage TO postgres;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA storage TO postgres;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA storage TO postgres;
|
||||
@@ -248,6 +248,31 @@ create policy "Only Blizzard staff can update leaderboard"
|
||||
);
|
||||
```
|
||||
|
||||
### Time to live for rows
|
||||
|
||||
Policies can also be used to implement TTL or time to live feature that you see in Instagram stories or Snapchat.
|
||||
In the following example, rows of `stories` table are available only if they have been created within the last 24 hours.
|
||||
|
||||
```sql
|
||||
-- 1. Create table
|
||||
create table if not exists stories (
|
||||
id uuid not null primary key DEFAULT uuid_generate_v4(),
|
||||
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
|
||||
content text not null
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table stories
|
||||
enable row level security;
|
||||
|
||||
-- 3. Create Policy
|
||||
create policy "Stories are live for a day"
|
||||
on stories
|
||||
for select using (
|
||||
created_at > (current_timestamp - interval '1 day')
|
||||
);
|
||||
```
|
||||
|
||||
### Advanced policies
|
||||
|
||||
Use the full power of SQL to build extremely advanced rules.
|
||||
|
||||
@@ -116,7 +116,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// Sign in user with email and password
|
||||
final response = await client
|
||||
.auth
|
||||
.signIn(email: 'example@email.com', password: 'example-password');
|
||||
@@ -170,7 +170,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// Sign in user with email and magic link
|
||||
final response = await client
|
||||
.auth
|
||||
.signIn(email: 'example@email.com');
|
||||
@@ -270,7 +270,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// Query tables and fetch related data in one round-trip, using select()
|
||||
final response = await client
|
||||
.from('countries')
|
||||
.select('name')
|
||||
@@ -320,7 +320,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// When fetching data, use advanced filtering to only extract data, that you need.
|
||||
final response = await client
|
||||
.from('cities')
|
||||
.select('name,country_id')
|
||||
@@ -374,7 +374,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// Create data easily, using insert()
|
||||
final response = await client
|
||||
.from('cities')
|
||||
.insert([
|
||||
@@ -430,7 +430,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// Listen to realtime database changes, using subscribe()
|
||||
final response = await client
|
||||
.from('countries')
|
||||
.on(SupabaseEventTypes.all, (payload) {
|
||||
@@ -483,7 +483,7 @@ import 'package:supabase/supabase.dart';
|
||||
void main() async {
|
||||
final client = SupabaseClient('supabaseUrl', 'supabaseKey');
|
||||
|
||||
// Sign up user with email and password
|
||||
// You can even listen to Row Level changes
|
||||
final response = await client
|
||||
.from('countries:id.eq.200')
|
||||
.on(SupabaseEventTypes.update, (payload) {
|
||||
|
||||
@@ -56,6 +56,7 @@ Build a basic Todo List with Supabase and your favorite frontend framework:
|
||||
- Flutter Supabase Authentication [Blog](https://www.sandromaglione.com/2021/04/24/flutter-supabase-authentication/)
|
||||
- Supabase in 6 minutes [Video](https://www.youtube.com/watch?v=c8DNV9yl0mg)
|
||||
- Let's build SupaAuth - Build an Authentication System using Supabase, Next.js and Typescript [6-part Blog Series](https://aalam.in/blog/supabase-auth-intro-setup-next)
|
||||
- In-depth self-hosting guide using Nginx [Blog](https://dev.to/chronsyn/self-hosting-with-supabase-1aii)
|
||||
|
||||
### Example apps
|
||||
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
---
|
||||
id: with-flutter
|
||||
title: "Quickstart: Flutter"
|
||||
description: Learn how to use Supabase in your Flutter App.
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## 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:
|
||||
|
||||
<img height="500px" src="/img/supabase-flutter-demo.png" alt="Supabase User Management example" />
|
||||
|
||||
### Github
|
||||
Whenever you get stuck at any point, take a look at [this repo](https://github.com/dshukertjr/flutter_supabase_quickstart).
|
||||
|
||||
## 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.io](https://app.supabase.io).
|
||||
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="UI"
|
||||
values={[
|
||||
{label: 'UI', value: 'UI'},
|
||||
{label: 'SQL', value: 'SQL'},
|
||||
]}>
|
||||
<TabItem value="UI">
|
||||
|
||||
```sh
|
||||
1. Go to the "SQL" section.
|
||||
2. Click "User Management Starter".
|
||||
3. Click "Run".
|
||||
```
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/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.
|
||||
|
||||
<Tabs
|
||||
defaultValue="UI"
|
||||
values={[
|
||||
{label: 'UI', value: 'UI'}
|
||||
]}>
|
||||
<TabItem value="UI">
|
||||
|
||||
```sh
|
||||
1. Go to the "Settings" section.
|
||||
2. Click "API" in the sidebar.
|
||||
3. Find your API URL in this page.
|
||||
4. Find your "anon" key on this page.
|
||||
```
|
||||
|
||||
<video width="99%" muted playsInline controls="true">
|
||||
<source src="/videos/api/api-url-and-key.mp4" type="video/mp4" muted playsInline />
|
||||
</video>
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Building the App
|
||||
|
||||
Let's start building the Flutter app from scratch.
|
||||
|
||||
### Initialize a Flutter app
|
||||
|
||||
We can use [`flutter create`](https://flutter.dev/docs/get-started/test-drive) to initialize
|
||||
an app called `supabase_quickstart`:
|
||||
|
||||
```bash
|
||||
flutter create supabase_quickstart
|
||||
```
|
||||
|
||||
Then let's install the only additional dependency: [supabase-flutter](https://github.com/supabase/supabase-flutter)
|
||||
|
||||
Open the app and copy and paste the following inside `pubspec.yaml` under `dependencies`.
|
||||
|
||||
```yaml
|
||||
supabase_flutter: ^0.0.4
|
||||
```
|
||||
|
||||
Run `flutter pub get` to install the dependencies.
|
||||
|
||||
### Setup deep links
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||

|
||||
|
||||
That is it on Supabase's end and the rest are platform specific settings:
|
||||
|
||||
For Android, add an intent-filter to enable deep linking:
|
||||
```xml title="android/app/src/main/AndroidManifest.xml"
|
||||
<manifest ...>
|
||||
<!-- ... other tags -->
|
||||
<application ...>
|
||||
<activity ...>
|
||||
<!-- ... other tags -->
|
||||
|
||||
<!-- Add this intent-filter for Deep Links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data
|
||||
android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="login-callback" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
For iOS add CFBundleURLTypes to enable deep linking:
|
||||
```xml title="ios/Runner/Info.plist"
|
||||
<!-- ... other tags -->
|
||||
<plist>
|
||||
<dict>
|
||||
<!-- ... other tags -->
|
||||
|
||||
<!-- Add this array for Deep Links -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>io.supabase.flutterquickstart</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- ... other tags -->
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
For web:
|
||||
|
||||
There are no additional configurations.
|
||||
|
||||
### Main function
|
||||
|
||||
Now that we have deep links ready let's initialize the Supabase client inside our `main` function with the API credentials that you copied [earlier](#get-the-api-keys).
|
||||
These variables will be exposed on the app, and that's completely fine since we have
|
||||
[Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database.
|
||||
|
||||
```dart title="lib/main.dart"
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Supabase.initialize(
|
||||
url: '[YOUR_SUPABASE_URL]',
|
||||
anonKey: '[YOUR_SUPABASE_ANNON_KEY]',
|
||||
authCallbackUrlHostname: 'login-callback',
|
||||
);
|
||||
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';
|
||||
|
||||
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
|
||||
@override
|
||||
void onUnauthenticated() {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
|
||||
@override
|
||||
void onAuthenticated(Session session) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/account', (route) => false);
|
||||
}
|
||||
|
||||
@override
|
||||
void onPasswordRecovery(Session session) {}
|
||||
|
||||
@override
|
||||
void onErrorAuthenticating(String message) {
|
||||
print('Error authenticating $message');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's also create a constant file to make it easier to use Supabase client.
|
||||
|
||||
```dart title="lib/utils/constants.dart"
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```dart title="lib/pages/splash_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter_guide/components/auth_state.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SplashPageState createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends AuthState<SplashPage> {
|
||||
@override
|
||||
void initState() {
|
||||
recoverSupabaseSession();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```dart title="lib/pages/login_page.dart"
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter_guide/components/auth_state.dart';
|
||||
import 'package:supabase/supabase.dart';
|
||||
import 'package:supabase_flutter_guide/utils/constants.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends AuthState<LoginPage> {
|
||||
bool _isLoading = false;
|
||||
late final TextEditingController _emailController;
|
||||
|
||||
Future<void> _signIn() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
final response = await supabase.auth.signIn(
|
||||
email: _emailController.text,
|
||||
options: AuthOptions(
|
||||
redirectTo: kIsWeb
|
||||
? null
|
||||
: 'io.supabase.flutterquickstart://login-callback/'));
|
||||
if (response.error != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(response.error!.message),
|
||||
backgroundColor: Colors.red,
|
||||
));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Check your email for login link!')));
|
||||
}
|
||||
setState(() {
|
||||
_emailController.clear();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_emailController = TextEditingController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Sign In')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
children: [
|
||||
const Text('Sign in via the magic link with your email below'),
|
||||
const SizedBox(height: 18),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(labelText: 'Email'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _signIn,
|
||||
child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Set up Account page
|
||||
|
||||
After a user is signed in we can allow them to edit their profile details and manage their account.
|
||||
Let's create a new component for that called `account_page.dart`.
|
||||
Note that this page will also inherit `AuthState` as it will handle user sign out.
|
||||
|
||||
```dart title="lib/pages/account_page.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter_guide/components/auth_state.dart';
|
||||
import 'package:supabase_flutter_guide/utils/constants.dart';
|
||||
|
||||
class AccountPage extends StatefulWidget {
|
||||
const AccountPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AccountPageState createState() => _AccountPageState();
|
||||
}
|
||||
|
||||
class _AccountPageState extends AuthState<AccountPage> {
|
||||
late final _usernameController = TextEditingController();
|
||||
late final _websiteController = TextEditingController();
|
||||
var _loading = false;
|
||||
|
||||
Future<void> _getProfile() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
final user = supabase.auth.user()!;
|
||||
final response = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
.execute();
|
||||
if (response.error != null && response.status != 406) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(response.error!.message)));
|
||||
}
|
||||
if (response.data != null) {
|
||||
_usernameController.text = response.data!['username'] as String;
|
||||
_websiteController.text = response.data!['website'] as String;
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateProfile() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
});
|
||||
final userName = _usernameController.text;
|
||||
final website = _websiteController.text;
|
||||
final user = supabase.auth.currentUser;
|
||||
final updates = {
|
||||
'id': user!.id,
|
||||
'username': userName,
|
||||
'website': website,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
final response = await supabase.from('profiles').upsert(updates).execute();
|
||||
if (response.error != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(response.error!.message),
|
||||
backgroundColor: Colors.red,
|
||||
));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Successfully updated profile!')));
|
||||
}
|
||||
setState(() {
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
final response = await supabase.auth.signOut();
|
||||
if (response.error != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(response.error!.message),
|
||||
backgroundColor: Colors.red,
|
||||
));
|
||||
}
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_getProfile();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_websiteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _usernameController,
|
||||
decoration: const InputDecoration(labelText: 'User Name'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextFormField(
|
||||
controller: _websiteController,
|
||||
decoration: const InputDecoration(labelText: 'Website'),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(
|
||||
onPressed: _updateProfile,
|
||||
child: Text(_loading ? 'Saving...' : 'Update')),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Launch!
|
||||
|
||||
Now that we have all the components in place, let's update `lib/main.dart`:
|
||||
|
||||
```dart title="lib/main.dart"
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:supabase_flutter_guide/pages/account_page.dart';
|
||||
import 'package:supabase_flutter_guide/pages/login_page.dart';
|
||||
import 'package:supabase_flutter_guide/pages/splash_page.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Supabase.initialize(
|
||||
url: '[YOUR_SUPABASE_URL]',
|
||||
anonKey: '[YOUR_SUPABASE_ANNON_KEY]',
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Supabase Flutter',
|
||||
theme: ThemeData.dark().copyWith(
|
||||
primaryColor: Colors.green,
|
||||
accentColor: Colors.green,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
onPrimary: Colors.white,
|
||||
primary: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
initialRoute: '/',
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/': (_) => const SplashPage(),
|
||||
'/login': (_) => const LoginPage(),
|
||||
'/account': (_) => const AccountPage(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once that's done, run this in a terminal window to launch on Android or iOS:
|
||||
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
Or for web, run the following command to launch it on `localhost:3000`
|
||||
|
||||
```bash
|
||||
flutter run -d web-server --web-hostname localhost --web-port 3000
|
||||
```
|
||||
|
||||
And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app.
|
||||
|
||||
|
||||
<img height="500px" src="/img/supabase-flutter-demo.png" alt="Supabase User Management example" />
|
||||
|
||||
## Next steps
|
||||
|
||||
At this stage you have a fully functional application!
|
||||
|
||||
- Got a question? [Ask here](https://github.com/supabase/supabase/discussions).
|
||||
- Sign in: [app.supabase.io](https://app.supabase.io)
|
||||
@@ -89,6 +89,7 @@ module.exports = {
|
||||
'guides/with-svelte',
|
||||
'guides/with-vue-3',
|
||||
'guides/with-angular',
|
||||
'guides/with-flutter',
|
||||
'guides/self-hosting',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -986,6 +986,7 @@ pages:
|
||||
- Policy permissions required:
|
||||
- `buckets` permissions: none
|
||||
- `objects` permissions: `insert`
|
||||
- For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
|
||||
examples:
|
||||
- name: Upload file
|
||||
isSpotlight: true
|
||||
@@ -1000,6 +1001,18 @@ pages:
|
||||
upsert: false
|
||||
})
|
||||
```
|
||||
- name: Upload file using `ArrayBuffer` from base64 file data
|
||||
js: |
|
||||
```js
|
||||
import {decode} from 'base64-arraybuffer'
|
||||
|
||||
const { data, error } = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.upload('public/avatar1.png', decode('base64FileData'), {
|
||||
contentType: 'image/png'
|
||||
})
|
||||
```
|
||||
|
||||
storage.from.update():
|
||||
title: 'from.update()'
|
||||
@@ -1008,6 +1021,7 @@ pages:
|
||||
- Policy permissions required:
|
||||
- `buckets` permissions: none
|
||||
- `objects` permissions: `update` and `select`
|
||||
- For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
|
||||
examples:
|
||||
- name: Update file
|
||||
isSpotlight: true
|
||||
@@ -1022,6 +1036,18 @@ pages:
|
||||
upsert: false
|
||||
})
|
||||
```
|
||||
- name: Update file using `ArrayBuffer` from base64 file data
|
||||
js: |
|
||||
```js
|
||||
import {decode} from 'base64-arraybuffer'
|
||||
|
||||
const { data, error } = await supabase
|
||||
.storage
|
||||
.from('avatars')
|
||||
.update('public/avatar1.png', decode('base64FileData'), {
|
||||
contentType: 'image/png'
|
||||
})
|
||||
```
|
||||
|
||||
storage.from.move():
|
||||
title: 'from.move()'
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -35,7 +35,7 @@ One of the best things about building Supabase is the community. We're even luck
|
||||
|
||||
### Where?
|
||||
|
||||
Blog post coming soon. Follow our [Twitter](twitter.com/supabase) to get notified immediately.
|
||||
Blog post coming soon. Follow our [Twitter](https://twitter.com/supabase) to get notified immediately.
|
||||
|
||||
## Tuesday 27 July: Auth v2
|
||||
|
||||
@@ -51,7 +51,7 @@ What's cooler than Arnie in the 90's? Passwordless logins. We've [already](https
|
||||
|
||||
### Where?
|
||||
|
||||
Blog post coming soon. Follow our [Twitter](twitter.com/supabase) to get notified immediately.
|
||||
Blog post coming soon. Follow our [Twitter](https://twitter.com/supabase) to get notified immediately.
|
||||
|
||||
## Wednesday 28 July: Storage v2
|
||||
|
||||
@@ -67,7 +67,7 @@ We shipped [Version 1 of Supabase Storage](https://supabase.io/blog/2021/03/30/s
|
||||
|
||||
### Where?
|
||||
|
||||
Blog post coming soon. Follow our [Twitter](twitter.com/supabase) to get notified immediately.
|
||||
Blog post coming soon. Follow our [Twitter](https://twitter.com/supabase) to get notified immediately.
|
||||
|
||||
## Thursday 29 July: Dashboard v2
|
||||
|
||||
@@ -83,7 +83,7 @@ Who needs a futuristic [Hoverboard from 2015](https://www.youtube.com/watch?v=NR
|
||||
|
||||
### Where?
|
||||
|
||||
Blog post coming soon. Follow our [Twitter](twitter.com/supabase) to get notified immediately.
|
||||
Blog post coming soon. Follow our [Twitter](https://twitter.com/supabase) to get notified immediately.
|
||||
|
||||
## Friday 30 July: One more thing
|
||||
|
||||
@@ -99,4 +99,4 @@ We all know that the best [SeQueLs](https://www.reddit.com/r/SequelMemes/) are a
|
||||
|
||||
### Where?
|
||||
|
||||
Blog post coming soon. Follow our [Twitter](twitter.com/supabase) to get notified immediately.
|
||||
Blog post coming soon. Follow our [Twitter](https://twitter.com/supabase) to get notified immediately.
|
||||
@@ -41,7 +41,7 @@ const CaseStudies = () => {
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<Benchmark />
|
||||
{/* <Benchmark /> */}
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,15 @@ function ExampleCard(props: any) {
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{!props.demo_url && (
|
||||
<Link href={props.repo_url} as={props.repo_url}>
|
||||
<a target="_blank">
|
||||
<Button size="tiny" type="default" iconRight={<IconArrowUpRight />}>
|
||||
View Code
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ const Developers = () => {
|
||||
<div className="col-span-6">
|
||||
<div className="m-3 mx-6">
|
||||
<Typography.Text type="secondary">Latest announcements</Typography.Text>
|
||||
<ul className="mt-6 space-y-16">
|
||||
<ul className="mt-6 space-y-3 pb-6">
|
||||
{AnnouncementsData.map((caseStudy: any, idx: number) => (
|
||||
<li className="flow-root" key={`flyout_case_${idx}`}>
|
||||
<a
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Button, Badge } from '@supabase/ui'
|
||||
import { Button, Badge, IconGitHub } from '@supabase/ui'
|
||||
import FlyOut from '~/components/UI/FlyOut'
|
||||
import Transition from 'lib/Transition'
|
||||
|
||||
@@ -239,19 +239,15 @@ const Nav = (props: Props) => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:flex items-center sm:space-x-4">
|
||||
<div className="hidden lg:flex items-center sm:space-x-3">
|
||||
<a href="https://github.com/supabase/supabase" target="_blank">
|
||||
<Button type="default" icon={<IconGitHub />}>
|
||||
GitHub
|
||||
</Button>
|
||||
</a>
|
||||
<a href="https://app.supabase.io/api/login">
|
||||
<Button>Start your project</Button>
|
||||
</a>
|
||||
<a
|
||||
href="https://app.supabase.io/api/login"
|
||||
className={`
|
||||
items-center px-1 border-b-2 border-transparent text-sm font-medium
|
||||
text-gray-500 hover:text-gray-700 hidden lg:block dark:text-dark-100 dark:hover:text-white
|
||||
`}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"></div> */}
|
||||
@@ -330,6 +326,13 @@ const Nav = (props: Props) => {
|
||||
>
|
||||
Pricing
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/supabase/supabase"
|
||||
target="_blank"
|
||||
className="block pl-3 pr-4 py-2 text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 dark:hover:bg-dark-600 hover:border-gray-300 dark:text-white"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<p className="mb-6 text-sm text-gray-400">Products available:</p>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
[
|
||||
{
|
||||
"type": "Announcement",
|
||||
"title": "Beta launch",
|
||||
"description": "We're excited to announce we have moved from Alpha to Beta",
|
||||
"title": "Launch Week II: The SQL",
|
||||
"description": "We're launching a product every day for one week in July.",
|
||||
"imgUrl": "images/case-study-monitoro.jpg",
|
||||
"logoUrl": "images/logos/monitoro.png",
|
||||
"organization": "Monitoro",
|
||||
"url": "/beta",
|
||||
"url": "/blog/2021/07/22/supabase-launch-week-sql",
|
||||
"postMeta": {
|
||||
"name": "Paul Copplestone",
|
||||
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
|
||||
"publishDate": "Mar 16, 2020",
|
||||
"readLength": 6
|
||||
},
|
||||
"ctaText": "View case study"
|
||||
},
|
||||
{
|
||||
"type": "Announcement",
|
||||
"title": "Supabase Storage",
|
||||
"description": "Supabase launches Storage leveraging Postgres.",
|
||||
"imgUrl": "images/case-study-monitoro.jpg",
|
||||
"logoUrl": "images/logos/monitoro.png",
|
||||
"organization": "Monitoro",
|
||||
"url": "/blog/2021/03/30/supabase-storage",
|
||||
"postMeta": {
|
||||
"name": "Paul Copplestone",
|
||||
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
{
|
||||
"name": "Local emulator",
|
||||
"description": "Develop locally and push to production when you're ready",
|
||||
"badge": "Q1 2021",
|
||||
"url": "https://supabasesubscribe.vercel.app/subscription/localEmulator"
|
||||
"badge": "",
|
||||
"url": "https://supabase.io/docs/guides/self-hosting"
|
||||
},
|
||||
{
|
||||
"name": "Supabase CLI",
|
||||
"description": "Manage Supabase projects from your local machine",
|
||||
"badge": "Q2 2021",
|
||||
"url": "https://supabasesubscribe.vercel.app/subscription/commandLine"
|
||||
"badge": "",
|
||||
"url": "https://github.com/supabase/cli"
|
||||
}
|
||||
]
|
||||
+36
-36
@@ -1,21 +1,9 @@
|
||||
[
|
||||
{
|
||||
"text": "Been playing with @supabase_io and so far I'm liking it. I ❤️ you get full access to the database. It's very quick to get up and running, seems a nice fit for prototypes or smaller apps",
|
||||
"url": "https://twitter.com/mattegreer/status/1341765395133415425",
|
||||
"handle": "mattegreer",
|
||||
"img_url": "/images/twitter-profiles/IgXniSVk_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Reply enjoying building on @supabase_io for a project with @lasnindrek - allows incredible development speed when bashing together an MVP",
|
||||
"url": "https://twitter.com/geordidearns/status/1344433863804788737",
|
||||
"handle": "geordidearns",
|
||||
"img_url": "/images/twitter-profiles/vppimY3M_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "OKAY OKAY FINE I'LL LOOK AT SUPABASE PROPERLY",
|
||||
"url": "https://twitter.com/benbarbersmith/status/1369758528764776453",
|
||||
"handle": "benbarbersmith",
|
||||
"img_url": "/images/twitter-profiles/Jqle_tE6_400x400.jpg"
|
||||
"text": "DB/Backend: @supabase_io Reason: Takes care of Auth, DB hosting, API. Open source, easy to eject into something else. I love SQL. Cloud IDE is incredible for productivity.",
|
||||
"url": "https://twitter.com/JessetheSibley/status/1369678185508593673",
|
||||
"handle": "JessetheSibley",
|
||||
"img_url": "/images/twitter-profiles/aNx9O0lH_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Been trying out this new stack. Loving it so far. Next.js (Frontend) + Stitches.js (Styling) + Radix (Primitives & Icons) + Supabase (Backend) + Vercel (Deployment) 😍 Thank you @vercel @Modulz @supabase_io for this amazing DX.",
|
||||
@@ -24,16 +12,28 @@
|
||||
"img_url": "/images/twitter-profiles/mZAIoZM1_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "DB/Backend: @supabase_io Reason: Takes care of Auth, DB hosting, API. Open source, easy to eject into something else. I love SQL. Cloud IDE is incredible for productivity.",
|
||||
"url": "https://twitter.com/JessetheSibley/status/1369678185508593673",
|
||||
"handle": "JessetheSibley",
|
||||
"img_url": "/images/twitter-profiles/aNx9O0lH_400x400.jpg"
|
||||
"text": "OKAY OKAY FINE I'LL LOOK AT SUPABASE PROPERLY",
|
||||
"url": "https://twitter.com/benbarbersmith/status/1369758528764776453",
|
||||
"handle": "benbarbersmith",
|
||||
"img_url": "/images/twitter-profiles/Jqle_tE6_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Looking for a Firebase alternative? Consider using the open-source @supabase_io. It's as simple as Firebase but I found the dashboard UX more intuitive and hey it's OSS! It's in beta so you can use it for free rn.",
|
||||
"url": "https://twitter.com/mneelansh15/status/1368171468153118721",
|
||||
"handle": "mneelansh15",
|
||||
"img_url": "/images/twitter-profiles/6XKH3Rm6_400x400.jpg"
|
||||
"text": "@supabase_io is amazing, super easy to get up and running quickly + they have multiple examples with various frameworks (chat app, auth, etc)",
|
||||
"url": "https://twitter.com/ssw1ss/status/1366515168260337671",
|
||||
"handle": "ssw1ss",
|
||||
"img_url": "/images/twitter-profiles/4mD0C5Wk_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Been playing with @supabase_io and so far I'm liking it. I ❤️ you get full access to the database. It's very quick to get up and running, seems a nice fit for prototypes or smaller apps",
|
||||
"url": "https://twitter.com/mattegreer/status/1341765395133415425",
|
||||
"handle": "mattegreer",
|
||||
"img_url": "/images/twitter-profiles/IgXniSVk_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Using @supabase_io with SWR by @vercel is something else 🔥",
|
||||
"url": "https://twitter.com/eunjae_lee/status/1369788285317419009",
|
||||
"handle": "eunjae_lee",
|
||||
"img_url": "/images/twitter-profiles/eGjSYbQd_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "big props to the @supabase_io team - open source is a two way street and they’re super responsive & community oriented",
|
||||
@@ -41,6 +41,12 @@
|
||||
"handle": "_mateomorris",
|
||||
"img_url": "/images/twitter-profiles/1FyEWRE8_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Enjoying building on @supabase_io for a project with @lasnindrek - allows incredible development speed when bashing together an MVP",
|
||||
"url": "https://twitter.com/geordidearns/status/1344433863804788737",
|
||||
"handle": "geordidearns",
|
||||
"img_url": "/images/twitter-profiles/vppimY3M_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Been actively working on Quill. It's been rapid, thanks to @supabase_io and @tailwindui",
|
||||
"url": "https://twitter.com/quill_web/status/1369061125157961734",
|
||||
@@ -53,12 +59,6 @@
|
||||
"handle": "MartinOrszulik",
|
||||
"img_url": "/images/twitter-profiles/ext7ggBo_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Using @supabase_io with SWR by @vercel is something else Fire",
|
||||
"url": "https://twitter.com/eunjae_lee/status/1369788285317419009",
|
||||
"handle": "eunjae_lee",
|
||||
"img_url": "/images/twitter-profiles/eGjSYbQd_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Another month, another impressive @supabase_io release. It’s rare to see a startup ship in public so consistently.",
|
||||
"url": "https://twitter.com/yuris/status/1366770633401491456",
|
||||
@@ -71,12 +71,6 @@
|
||||
"handle": "swyx",
|
||||
"img_url": "/images/twitter-profiles/qhvO9V6x_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "@supabase_io is amazing, super easy to get up and running quickly + they have multiple examples with various frameworks (chat app, auth, etc)",
|
||||
"url": "https://twitter.com/ssw1ss/status/1366515168260337671",
|
||||
"handle": "ssw1ss",
|
||||
"img_url": "/images/twitter-profiles/4mD0C5Wk_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "This weekend I made a personal record 🥇 on the less time spent creating an application with social login / permissions, database, cdn, infinite scaling, git push to deploy and for free. Thanks to @supabase_io and @vercel",
|
||||
"url": "https://twitter.com/jperelli/status/1366195769657720834",
|
||||
@@ -107,6 +101,12 @@
|
||||
"handle": "prakis",
|
||||
"img_url": "/images/twitter-profiles/eRvqp-t0_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Looking for a Firebase alternative? Consider using the open-source @supabase_io. It's as simple as Firebase but I found the dashboard UX more intuitive and hey it's OSS! It's in beta so you can use it for free rn.",
|
||||
"url": "https://twitter.com/mneelansh15/status/1368171468153118721",
|
||||
"handle": "mneelansh15",
|
||||
"img_url": "/images/twitter-profiles/6XKH3Rm6_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"text": "Me using @supabase_io for the first time right now 🤯",
|
||||
"url": "https://twitter.com/nasiscoe/status/1365140856035024902",
|
||||
|
||||
+9
-15
@@ -118,20 +118,15 @@ function AuthPage() {
|
||||
</div>
|
||||
<img src={`${basePath}/images/product/auth/gitlab-icon.svg`} width={21} />
|
||||
<img src={`${basePath}/images/product/auth/bitbucket-icon.svg`} width={21} />
|
||||
<img src={`${basePath}/images/product/auth/twitter-icon.svg`} width={21} />
|
||||
</Space>
|
||||
</p>
|
||||
<Typography.Title level={4}>All the social providers</Typography.Title>
|
||||
<Typography.Text>
|
||||
<p className="text-base lg:text-lg">
|
||||
Enable social logins with the click of a button. Google, Facebook, Github, Azure,
|
||||
Gitlab and Bitbucket.
|
||||
<p className="text-base">
|
||||
Enable social logins with the click of a button. Google, Facebook, GitHub, Azure,
|
||||
Gitlab, Twitter, Discord, and Bitbucket.
|
||||
</p>
|
||||
<Space>
|
||||
<img src={`${basePath}/images/product/auth/twitter-icon.svg`} width={21} />
|
||||
<p className="m-0">
|
||||
<span className="text-gray-800 dark:text-white">Twitter</span> coming soon
|
||||
</p>
|
||||
</Space>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="mb-10 lg:mb-0 col-span-12 lg:col-span-3 lg:col-start-5">
|
||||
@@ -142,10 +137,10 @@ function AuthPage() {
|
||||
</Typography.Text>
|
||||
<Typography.Title level={4}>Fully integrated</Typography.Title>
|
||||
<Typography.Text>
|
||||
<p className="text-base lg:text-lg">
|
||||
Incredibly simple Auth, without a single external authentication service.
|
||||
<p className="text-base">
|
||||
Incredibly simple Auth, without a single external authentication service. Built-in
|
||||
Authentication, Authorization, and User Management.
|
||||
</p>
|
||||
<p>Built-in Authentication, Authorization, and User Management.</p>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-3 lg:col-start-9">
|
||||
@@ -156,11 +151,10 @@ function AuthPage() {
|
||||
</Typography.Text>
|
||||
<Typography.Title level={4}>Own your data</Typography.Title>
|
||||
<Typography.Text>
|
||||
<p className="text-base lg:text-lg">
|
||||
<p className="text-base">
|
||||
User data stored in your Supabase database so you never have to worry about 3rd
|
||||
party privacy issues.
|
||||
party privacy issues. Host your data in 8 different locations.
|
||||
</p>
|
||||
<p>Host your data in 8 different regions.</p>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,6 +117,9 @@ const Hero = () => {
|
||||
<p className="text-2xl">
|
||||
Today, we're moving to <span className="text-brand-700">Beta</span>
|
||||
</p>
|
||||
<time itemProp="datePublished" dateTime="2020-12-03" className="opacity-50 text-sm">
|
||||
Published December 3rd, 2020
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +155,11 @@ function Database() {
|
||||
<div className="col-span-12 lg:col-span-3 lg:col-start-9">
|
||||
<Typography.Text>
|
||||
<p className="mb-4">
|
||||
<IconShuffle />
|
||||
<Space>
|
||||
<ProductIcon icon={Solutions['database'].icon} />
|
||||
<IconX />
|
||||
<ProductIcon icon={'M13 10V3L4 14h7v7l9-11h-7z'} />
|
||||
</Space>
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<Typography.Title level={4}>Realtime enabled</Typography.Title>
|
||||
|
||||
@@ -35,7 +35,7 @@ function StoragePage() {
|
||||
|
||||
const meta_title = 'Storage | Store any digital content'
|
||||
const meta_description =
|
||||
'An open source Object store service with unlimited scalability, for any file type.'
|
||||
'An open source Object store with unlimited scalability, for any file type.'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,7 +64,7 @@ function StoragePage() {
|
||||
</span>,
|
||||
]}
|
||||
subheader={[
|
||||
'An open source Object store service with unlimited scalability, for any file type.',
|
||||
'An open source Object store with unlimited scalability, for any file type.',
|
||||
'With custom policies and permissions that are familiar and easy to implement.',
|
||||
]}
|
||||
image={[
|
||||
@@ -116,8 +116,14 @@ function StoragePage() {
|
||||
</div>
|
||||
<div className="mb-10 lg:mb-0 col-span-12 lg:col-span-3 lg:col-start-5">
|
||||
<Typography.Text>
|
||||
<p className="mb-4">
|
||||
<IconBarChart />
|
||||
<p>
|
||||
<Space>
|
||||
<ProductIcon
|
||||
icon={
|
||||
'M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<Typography.Title level={4}>Lightning fast</Typography.Title>
|
||||
@@ -133,8 +139,14 @@ function StoragePage() {
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-3 lg:col-start-9">
|
||||
<Typography.Text>
|
||||
<p className="mb-4">
|
||||
<IconCheckSquare />
|
||||
<p>
|
||||
<Space>
|
||||
<ProductIcon
|
||||
icon={
|
||||
'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z'
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<Typography.Title level={4}>Dependable</Typography.Title>
|
||||
@@ -201,11 +213,11 @@ function StoragePage() {
|
||||
</p>,
|
||||
]}
|
||||
footer={[
|
||||
<div className="grid grid-cols-12 gap-8 lg:gap-0 xl:gap-16 mt-8">
|
||||
<div className="grid grid-cols-12 gap-8 lg:gap-0 xl:gap-16 my-8">
|
||||
<div className="col-span-6 lg:col-span-12 lg:mb-8 xl:mb-0 xl:col-span-4">
|
||||
<FeatureColumn
|
||||
icon={<IconWifi />}
|
||||
title="CDN integration"
|
||||
title="CDN"
|
||||
text="Serve from the edge to reduce latency."
|
||||
/>
|
||||
<Badge color="blue">Coming soon</Badge>
|
||||
@@ -213,7 +225,7 @@ function StoragePage() {
|
||||
<div className="col-span-6 lg:col-span-12 xl:col-span-4">
|
||||
<FeatureColumn
|
||||
icon={<IconShuffle />}
|
||||
title="Auto transformation & optimization"
|
||||
title="Transformations"
|
||||
text="Resize and compress your media before you serve it."
|
||||
/>
|
||||
<Badge color="blue">Coming soon</Badge>
|
||||
@@ -243,7 +255,7 @@ function StoragePage() {
|
||||
with Supabase Auth
|
||||
</Typography.Title>
|
||||
<Typography.Text>
|
||||
<p className="text-lg">
|
||||
<p className="">
|
||||
Using Postgres Row Level Security to create Object access rules.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
Reference in new issue
Block a user