diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 194050ee191..e0c4af21e53 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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} \ No newline at end of file diff --git a/docker/dockerfiles/kong/kong.yml b/docker/dockerfiles/kong/kong.yml index 1adfb5c8c58..659d804783e 100644 --- a/docker/dockerfiles/kong/kong.yml +++ b/docker/dockerfiles/kong/kong.yml @@ -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: diff --git a/docker/dockerfiles/postgres/Dockerfile b/docker/dockerfiles/postgres/Dockerfile index cbec306808c..cd03b29b241 100644 --- a/docker/dockerfiles/postgres/Dockerfile +++ b/docker/dockerfiles/postgres/Dockerfile @@ -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 diff --git a/docker/dockerfiles/postgres/storage-schema.sql b/docker/dockerfiles/postgres/storage-schema.sql new file mode 100644 index 00000000000..c879c6b6234 --- /dev/null +++ b/docker/dockerfiles/postgres/storage-schema.sql @@ -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; \ No newline at end of file diff --git a/web/docs/guides/auth.mdx b/web/docs/guides/auth.mdx index 5e488a89fe4..8fd47316bd9 100644 --- a/web/docs/guides/auth.mdx +++ b/web/docs/guides/auth.mdx @@ -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. diff --git a/web/docs/guides/client-libraries.mdx b/web/docs/guides/client-libraries.mdx index 3daeea8ba50..8bbbf6e4488 100755 --- a/web/docs/guides/client-libraries.mdx +++ b/web/docs/guides/client-libraries.mdx @@ -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) { diff --git a/web/docs/guides/examples.mdx b/web/docs/guides/examples.mdx index 90a84f66f7d..efde677ad7b 100644 --- a/web/docs/guides/examples.mdx +++ b/web/docs/guides/examples.mdx @@ -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 diff --git a/web/docs/guides/with-flutter.mdx b/web/docs/guides/with-flutter.mdx new file mode 100644 index 00000000000..b1bcf43468d --- /dev/null +++ b/web/docs/guides/with-flutter.mdx @@ -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). + +- [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: + +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. + + + + +```sh +1. Go to the "SQL" section. +2. Click "User Management Starter". +3. Click "Run". +``` + + + + + + +```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' ); + +``` + + + + + +### 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. + + + + +```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. +``` + + + + + + +## 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. +``` + +![Supabase console deep link setting](/img/deeplink-setting.png) + +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" + + + + + + + + + + + + + + + + + + +``` + +For iOS add CFBundleURLTypes to enable deep linking: +```xml title="ios/Runner/Info.plist" + + + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + io.supabase.flutterquickstart + + + + + + +``` + +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 extends SupabaseAuthState { + @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 { + @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 { + bool _isLoading = false; + late final TextEditingController _emailController; + + Future _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 { + late final _usernameController = TextEditingController(); + late final _websiteController = TextEditingController(); + var _loading = false; + + Future _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 _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 _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: { + '/': (_) => 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. + + +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) diff --git a/web/sidebars.js b/web/sidebars.js index 3e4c4533af6..7b387d05ece 100755 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -89,6 +89,7 @@ module.exports = { 'guides/with-svelte', 'guides/with-vue-3', 'guides/with-angular', + 'guides/with-flutter', 'guides/self-hosting', ], }, diff --git a/web/spec/supabase.yml b/web/spec/supabase.yml index b6e3a05e9cc..45f4e397478 100644 --- a/web/spec/supabase.yml +++ b/web/spec/supabase.yml @@ -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()' diff --git a/web/static/img/deeplink-setting.png b/web/static/img/deeplink-setting.png new file mode 100644 index 00000000000..ebfa4cce24a Binary files /dev/null and b/web/static/img/deeplink-setting.png differ diff --git a/web/static/img/supabase-flutter-demo.png b/web/static/img/supabase-flutter-demo.png new file mode 100644 index 00000000000..11653b261b8 Binary files /dev/null and b/web/static/img/supabase-flutter-demo.png differ diff --git a/www/_blog/2021-07-22-supabase-launch-week-sql.mdx b/www/_blog/2021-07-22-supabase-launch-week-sql.mdx index c0c9c2f1d68..4bc4c6fbdf2 100644 --- a/www/_blog/2021-07-22-supabase-launch-week-sql.mdx +++ b/www/_blog/2021-07-22-supabase-launch-week-sql.mdx @@ -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. diff --git a/www/components/CaseStudies/index.tsx b/www/components/CaseStudies/index.tsx index 191db508e53..078192c5074 100644 --- a/www/components/CaseStudies/index.tsx +++ b/www/components/CaseStudies/index.tsx @@ -41,7 +41,7 @@ const CaseStudies = () => { ))} - + {/* */} ) } diff --git a/www/components/ExampleCard.tsx b/www/components/ExampleCard.tsx index da2286f3299..b937d6c678b 100644 --- a/www/components/ExampleCard.tsx +++ b/www/components/ExampleCard.tsx @@ -72,6 +72,15 @@ function ExampleCard(props: any) { )} + {!props.demo_url && ( + + + + + + )} diff --git a/www/components/Nav/Developers.tsx b/www/components/Nav/Developers.tsx index 1a48ee169cf..89c88745fa3 100644 --- a/www/components/Nav/Developers.tsx +++ b/www/components/Nav/Developers.tsx @@ -66,7 +66,7 @@ const Developers = () => {
Latest announcements -
    +
      {AnnouncementsData.map((caseStudy: any, idx: number) => (
    • {
- {/*
*/} @@ -330,6 +326,13 @@ const Nav = (props: Props) => { > Pricing + + GitHub +

Products available:

diff --git a/www/data/Announcements.json b/www/data/Announcements.json index d89a6d8d68b..4145701af75 100644 --- a/www/data/Announcements.json +++ b/www/data/Announcements.json @@ -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", diff --git a/www/data/DeveloperFeatures.json b/www/data/DeveloperFeatures.json index c04d5e06ad7..0f6c00fe224 100644 --- a/www/data/DeveloperFeatures.json +++ b/www/data/DeveloperFeatures.json @@ -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" } ] diff --git a/www/data/tweets/Tweets.json b/www/data/tweets/Tweets.json index 1f0482436e0..a0da5737f65 100644 --- a/www/data/tweets/Tweets.json +++ b/www/data/tweets/Tweets.json @@ -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", diff --git a/www/pages/auth/Auth.tsx b/www/pages/auth/Auth.tsx index 5302f45f424..f3c846e2195 100644 --- a/www/pages/auth/Auth.tsx +++ b/www/pages/auth/Auth.tsx @@ -118,20 +118,15 @@ function AuthPage() {
+

All the social providers -

- Enable social logins with the click of a button. Google, Facebook, Github, Azure, - Gitlab and Bitbucket. +

+ Enable social logins with the click of a button. Google, Facebook, GitHub, Azure, + Gitlab, Twitter, Discord, and Bitbucket.

- - -

- Twitter coming soon -

-
@@ -142,10 +137,10 @@ function AuthPage() { Fully integrated -

- Incredibly simple Auth, without a single external authentication service. +

+ Incredibly simple Auth, without a single external authentication service. Built-in + Authentication, Authorization, and User Management.

-

Built-in Authentication, Authorization, and User Management.

@@ -156,11 +151,10 @@ function AuthPage() { Own your data -

+

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.

-

Host your data in 8 different regions.

diff --git a/www/pages/beta.tsx b/www/pages/beta.tsx index b1137c7ea98..350c3884c1f 100644 --- a/www/pages/beta.tsx +++ b/www/pages/beta.tsx @@ -117,6 +117,9 @@ const Hero = () => {

Today, we're moving to Beta

+ diff --git a/www/pages/database/Database.tsx b/www/pages/database/Database.tsx index aa9482b40b2..df51e42e25d 100644 --- a/www/pages/database/Database.tsx +++ b/www/pages/database/Database.tsx @@ -155,7 +155,11 @@ function Database() {

- + + + + +

Realtime enabled diff --git a/www/pages/storage/Storage.tsx b/www/pages/storage/Storage.tsx index 20289f61add..2e027bd8b14 100644 --- a/www/pages/storage/Storage.tsx +++ b/www/pages/storage/Storage.tsx @@ -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() { , ]} 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() {
-

- +

+ + +

Lightning fast @@ -133,8 +139,14 @@ function StoragePage() {
-

- +

+ + +

Dependable @@ -201,11 +213,11 @@ function StoragePage() {

, ]} footer={[ -
+
} - title="CDN integration" + title="CDN" text="Serve from the edge to reduce latency." /> Coming soon @@ -213,7 +225,7 @@ function StoragePage() {
} - title="Auto transformation & optimization" + title="Transformations" text="Resize and compress your media before you serve it." /> Coming soon @@ -243,7 +255,7 @@ function StoragePage() { with Supabase Auth -

+

Using Postgres Row Level Security to create Object access rules.