- Flutter app (Android/iOS): home, explore, albums, search, player with equalizer, mini player, sign up/sign in with email codes, profile, encrypted offline downloads, About page - server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx (or Caddy), migrations, backups, email templates - supabase/migrations: schema, row-level security, profiles - tools/: song scanner and importer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
238 lines
11 KiB
PL/PgSQL
238 lines
11 KiB
PL/PgSQL
-- Nanga Kathai — initial schema.
|
|
-- Every song is hidden from listeners until an admin approves it (status = 'approved').
|
|
|
|
create extension if not exists pg_trgm;
|
|
|
|
-- ---------------------------------------------------------------- types
|
|
create type user_role as enum ('listener', 'artist', 'admin');
|
|
create type track_status as enum ('draft', 'pending', 'needs_changes', 'approved', 'rejected', 'taken_down');
|
|
create type rights_status as enum ('legacy_unverified', 'artist_approved', 'licensed');
|
|
create type track_tier as enum ('free', 'premium');
|
|
create type lyrics_script as enum ('tamil', 'latin', 'kannada', 'english');
|
|
|
|
-- ---------------------------------------------------------------- users
|
|
create table profiles (
|
|
id uuid primary key references auth.users on delete cascade,
|
|
display_name text,
|
|
role user_role not null default 'listener',
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create function handle_new_user() returns trigger
|
|
language plpgsql security definer set search_path = public as $$
|
|
begin
|
|
insert into profiles (id, display_name) values (new.id, split_part(new.email, '@', 1));
|
|
return new;
|
|
end $$;
|
|
|
|
create trigger on_auth_user_created after insert on auth.users
|
|
for each row execute function handle_new_user();
|
|
|
|
create function is_admin() returns boolean
|
|
language sql stable security definer set search_path = public as $$
|
|
select exists (select 1 from profiles where id = auth.uid() and role = 'admin');
|
|
$$;
|
|
|
|
-- ---------------------------------------------------------------- catalog
|
|
create table hattis (
|
|
id uuid primary key default gen_random_uuid(),
|
|
name_latin text not null unique,
|
|
name_tamil text,
|
|
lat double precision,
|
|
lng double precision
|
|
);
|
|
|
|
create table artists (
|
|
id uuid primary key default gen_random_uuid(),
|
|
name_latin text not null,
|
|
name_tamil text,
|
|
hatti_id uuid references hattis,
|
|
bio text,
|
|
photo_url text,
|
|
photo_permission boolean not null default false,
|
|
verified boolean not null default false,
|
|
claimed_by uuid references profiles,
|
|
created_at timestamptz not null default now(),
|
|
-- A singer photo is only stored once permission is on record.
|
|
constraint photo_needs_permission check (photo_url is null or photo_permission)
|
|
);
|
|
|
|
create table albums (
|
|
id uuid primary key default gen_random_uuid(),
|
|
title_latin text not null,
|
|
title_tamil text,
|
|
year int,
|
|
cover_url text
|
|
);
|
|
|
|
create table tracks (
|
|
id uuid primary key default gen_random_uuid(),
|
|
title_latin text not null,
|
|
title_tamil text,
|
|
title_en_meaning text,
|
|
album_id uuid references albums,
|
|
occasion text not null default 'folk'
|
|
check (occasion in ('huttu','habba','madhuvae','devotional','folk','saavu','instrumental')),
|
|
year int,
|
|
duration_ms int,
|
|
audio_key_64 text, -- object keys in the R2 bucket
|
|
audio_key_128 text,
|
|
audio_key_256 text,
|
|
loudness_lufs real,
|
|
cover_url text,
|
|
tier track_tier not null default 'free',
|
|
status track_status not null default 'pending',
|
|
rights_status rights_status not null default 'legacy_unverified',
|
|
source_note text, -- e.g. "Old collection, cassette 12 side A"
|
|
reviewed_by uuid references profiles,
|
|
reviewed_at timestamptz,
|
|
review_note text,
|
|
created_at timestamptz not null default now(),
|
|
constraint approved_needs_audio check (status <> 'approved' or audio_key_128 is not null)
|
|
);
|
|
|
|
create index tracks_status_idx on tracks (status);
|
|
create index tracks_occasion_idx on tracks (occasion) where status = 'approved';
|
|
create index tracks_title_trgm on tracks using gin (title_latin gin_trgm_ops);
|
|
|
|
create table track_artists (
|
|
track_id uuid references tracks on delete cascade,
|
|
artist_id uuid references artists on delete cascade,
|
|
role text not null default 'singer' check (role in ('singer','lyricist','composer','musician')),
|
|
position int not null default 0,
|
|
primary key (track_id, artist_id, role)
|
|
);
|
|
|
|
create table lyrics (
|
|
track_id uuid references tracks on delete cascade,
|
|
script lyrics_script not null,
|
|
body text not null,
|
|
synced_lrc text,
|
|
primary key (track_id, script)
|
|
);
|
|
|
|
create table festivals (
|
|
id uuid primary key default gen_random_uuid(),
|
|
name text not null,
|
|
next_date date,
|
|
description text
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- rights
|
|
create table consents (
|
|
id uuid primary key default gen_random_uuid(),
|
|
artist_id uuid references artists on delete cascade,
|
|
track_id uuid references tracks on delete cascade,
|
|
consent_type text not null check (consent_type in ('song_distribution','photo','bio')),
|
|
given_by text not null, -- singer or family member's name
|
|
evidence_url text, -- signed form / message screenshot in private storage
|
|
recorded_by uuid references profiles,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table takedown_requests (
|
|
id uuid primary key default gen_random_uuid(),
|
|
track_id uuid references tracks on delete cascade,
|
|
requester text not null,
|
|
contact text not null,
|
|
reason text not null,
|
|
status text not null default 'open' check (status in ('open','actioned','dismissed')),
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- listener data
|
|
create table user_likes (
|
|
user_id uuid references profiles on delete cascade,
|
|
track_id uuid references tracks on delete cascade,
|
|
created_at timestamptz not null default now(),
|
|
primary key (user_id, track_id)
|
|
);
|
|
|
|
create table playlists (
|
|
id uuid primary key default gen_random_uuid(),
|
|
owner_id uuid references profiles on delete cascade,
|
|
title text not null,
|
|
is_public boolean not null default false,
|
|
is_editorial boolean not null default false,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table playlist_items (
|
|
playlist_id uuid references playlists on delete cascade,
|
|
track_id uuid references tracks on delete cascade,
|
|
position int not null,
|
|
added_at timestamptz not null default now(),
|
|
primary key (playlist_id, track_id)
|
|
);
|
|
|
|
create table play_events (
|
|
id bigint generated always as identity primary key,
|
|
user_id uuid references profiles on delete set null,
|
|
track_id uuid references tracks on delete cascade,
|
|
started_at timestamptz not null default now(),
|
|
ms_played int,
|
|
source text,
|
|
offline boolean not null default false
|
|
);
|
|
|
|
-- ---------------------------------------------------------------- row level security
|
|
alter table profiles enable row level security;
|
|
alter table hattis enable row level security;
|
|
alter table artists enable row level security;
|
|
alter table albums enable row level security;
|
|
alter table tracks enable row level security;
|
|
alter table track_artists enable row level security;
|
|
alter table lyrics enable row level security;
|
|
alter table festivals enable row level security;
|
|
alter table consents enable row level security;
|
|
alter table takedown_requests enable row level security;
|
|
alter table user_likes enable row level security;
|
|
alter table playlists enable row level security;
|
|
alter table playlist_items enable row level security;
|
|
alter table play_events enable row level security;
|
|
|
|
-- Profiles: read and edit your own; admins see all. Role changes are admin-only.
|
|
create policy own_profile_read on profiles for select using (id = auth.uid() or is_admin());
|
|
create policy own_profile_update on profiles for update using (id = auth.uid())
|
|
with check (id = auth.uid() and role = (select role from profiles where id = auth.uid()));
|
|
create policy admin_profiles on profiles for all using (is_admin()) with check (is_admin());
|
|
|
|
-- Public catalog: anyone (including guests) reads approved songs only.
|
|
create policy public_read on hattis for select using (true);
|
|
create policy public_read on artists for select using (true);
|
|
create policy public_read on albums for select using (true);
|
|
create policy public_read on festivals for select using (true);
|
|
create policy approved_read on tracks for select using (status = 'approved' or is_admin());
|
|
create policy approved_read on track_artists for select
|
|
using (exists (select 1 from tracks t where t.id = track_id and (t.status = 'approved' or is_admin())));
|
|
create policy approved_read on lyrics for select
|
|
using (exists (select 1 from tracks t where t.id = track_id and (t.status = 'approved' or is_admin())));
|
|
|
|
-- Admins manage the catalog and rights records.
|
|
create policy admin_write on hattis for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on artists for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on albums for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on tracks for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on track_artists for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on lyrics for all using (is_admin()) with check (is_admin());
|
|
create policy admin_write on festivals for all using (is_admin()) with check (is_admin());
|
|
create policy admin_only on consents for all using (is_admin()) with check (is_admin());
|
|
|
|
-- Anyone may file a takedown request; only admins read them.
|
|
create policy file_takedown on takedown_requests for insert with check (true);
|
|
create policy admin_takedowns on takedown_requests for select using (is_admin());
|
|
create policy admin_takedowns_update on takedown_requests for update using (is_admin());
|
|
|
|
-- Listener data belongs to the listener.
|
|
create policy own_likes on user_likes for all using (user_id = auth.uid()) with check (user_id = auth.uid());
|
|
create policy own_or_public_playlists on playlists for select using (owner_id = auth.uid() or is_public or is_admin());
|
|
create policy own_playlists on playlists for all using (owner_id = auth.uid()) with check (owner_id = auth.uid() and not is_editorial);
|
|
create policy admin_playlists on playlists for all using (is_admin()) with check (is_admin());
|
|
create policy playlist_items_read on playlist_items for select
|
|
using (exists (select 1 from playlists p where p.id = playlist_id and (p.owner_id = auth.uid() or p.is_public or is_admin())));
|
|
create policy playlist_items_write on playlist_items for all
|
|
using (exists (select 1 from playlists p where p.id = playlist_id and (p.owner_id = auth.uid() or is_admin())))
|
|
with check (exists (select 1 from playlists p where p.id = playlist_id and (p.owner_id = auth.uid() or is_admin())));
|
|
create policy log_own_plays on play_events for insert with check (user_id = auth.uid());
|
|
create policy admin_plays on play_events for select using (is_admin());
|