In this tutorial we will see how easy and quick it is to deploy an application on to DigitalOcean via App Platform with a self-hosted Supabase instance also running on DigitalOcean.
The focus of this repository is to deploy the following resources:
The application is a simple user management app (taken from the Supabase web app tutorials) written in React.
With our final architecture looking like so:
Before deploying the application we need to set up the database schema. Copy the following SQL and paste it in the SQL Editor within your Supabase instance (Click the little arrowhead to expand and see the code).
-- Create a table for public profiles
create table profiles (
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
full_name text,
avatar_url text,
website text,
constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles
enable row level security;
create policy "Public profiles are viewable by everyone." on profiles
for select using (true);
create policy "Users can insert their own profile." on profiles
for insert with check (auth.uid() = id);
create policy "Users can update own profile." on profiles
for update using (auth.uid() = id);
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
-- Set up access controls for storage.
-- See https://supabase.com/docs/guides/storage#policy-examples for more details.
create policy "Avatar images are publicly accessible." on storage.objects
for select using (bucket_id = 'avatars');
create policy "Anyone can upload an avatar." on storage.objects
for insert with check (bucket_id = 'avatars');
create policy "Anyone can update their own avatar." on storage.objects
for update using (auth.uid() = owner) with check (bucket_id = 'avatars');
To efficiently use our Spaces bucket we will allow database triggers to remove obsolete objects.
First thing to do is enable the http
extension for the extension
schema in the Supabase Dashboard as shown below.
Then, define the following SQL functions in the SQL Editor to delete storage objects via the API. N.B. Replace the variables <YOUR_SUPABASE_URL>
and <YOUR_SERVICE_ROLE_KEY>
with your information.
create or replace function delete_storage_object(bucket text, object text, out status int, out content varchar)
returns record
language 'plpgsql'
security definer
as $$
declare
project_url varchar := '<YOUR_SUPABASE_URL>';
service_role_key varchar := '<YOUR_SERVICE_ROLE_KEY>'; -- full access needed
url varchar := project_url||'/storage/v1/object/'||bucket||'/'||object;
begin
select
into status, content
result.status::int, result.content::varchar
FROM extensions.http((
'DELETE',
url,
ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)],
NULL,
NULL)::extensions.http_request) as result;
end;
$$;
create or replace function delete_avatar(avatar_url text, out status int, out content varchar)
returns record
language 'plpgsql'
security definer
as $$
begin
select
into status, content
result.status, result.content
from public.delete_storage_object('avatars', avatar_url) as result;
end;
$$;
Next, add a trigger that removes any obsolete avatars whenever the profile is updated or deleted:
create or replace function delete_old_avatar()
returns trigger
language 'plpgsql'
security definer
as $$
declare
status int;
content varchar;
begin
if coalesce(old.avatar_url, '') <> ''
and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then
select
into status, content
result.status, result.content
from public.delete_avatar(old.avatar_url) as result;
if status <> 200 then
raise warning 'Could not delete avatar: % %', status, content;
end if;
end if;
if tg_op = 'DELETE' then
return old;
end if;
return new;
end;
$$;
create trigger before_profile_changes
before update of avatar_url or delete on public.profiles
for each row execute function public.delete_old_avatar();
Finally, delete the public.profile
row before a user is deleted. If this step is omitted, you won’t be able to delete users without first manually deleting their avatar image.
create or replace function delete_old_profile()
returns trigger
language 'plpgsql'
security definer
as $$
begin
delete from public.profiles where id = old.id;
return old;
end;
$$;
create trigger before_delete_user
before delete on auth.users
for each row execute function public.delete_old_profile();
Once we’ve set up the schema and enabled the appropriate extensions and triggers we can deploy our app. This is as easy as a few clicks of a button (no really click the button below).
When you get to the Environment Variables
section input the data with your information, confirm the Information in the next sections are what you wish and correct (Plan, Name, Region, Project, etc), and once at the Review section click the Create Resources
button at the bottom of the page.
Once the application has been successfully deployed you’ll be able to access it via the domain generated for you, or the domain you specified before creation (more info in the next section).
Once the app launches you’ll see the sign in page. Entering an email will send a magic link to your inbox and following the link will allow you update your profile and upload a profile pic. You’ll see all of this information within your Supabase instance under the appropriate sections (Table Editor, Database, Authentication, Storage etc.)
App Platform can automatically manage your domain for you and create a CNAME that points your domain to the DO provided domain (sub-domain on ondigitalocean.app
) for your application. You can either do this by modifying the deploy.template.yaml
file provided with your domain information or manually as explained through this documentation.
In this blog we should you how easy it is to deploy a React app on App Platform with a self-hosted Supabase instance. This allows you to focus on what is important, creating an amazing application for your end users and leaving the management of the underlying infrastructure and application deployment to DigitalOcean.
All that’s needed now is your great idea!!
Enjoy and Happy creating :)