Supabase setup (required for admin and client booking/payments)
Create the two admin users under Authentication β Users with passwords set privately. If you already ran the earlier dashboard setup, run the additional alter/policy setup below in the SQL editor too. This enables public bookings and private receipt uploads; passwords remain handled by Supabase.
create table if not exists public.bookings (
id text primary key,
client text not null,
phone text not null,
service text not null,
event_date date not null,
amount numeric(12,2) not null check (amount >= 0),
paid boolean not null default false,
created_at timestamptz not null default now()
);
alter table public.bookings enable row level security;
grant select, insert, update, delete on public.bookings to authenticated;
drop policy if exists "No Limits admins manage bookings" on public.bookings;
create policy "No Limits admins manage bookings" on public.bookings
for all to authenticated
using (lower(coalesce(auth.jwt() ->> 'email', '')) in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com'))
with check (lower(coalesce(auth.jwt() ->> 'email', '')) in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com'));
grant select, insert, delete on public.bookings to authenticated;
grant update (paid, cancellation_status) on public.bookings to authenticated;
drop policy if exists "No Limits admins update booking payment and cancellation" on public.bookings;
create policy "No Limits admins update booking payment and cancellation"
on public.bookings
for update to authenticated
using (
lower(coalesce(auth.jwt() ->> 'email', ''))
in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com')
)
with check (
lower(coalesce(auth.jwt() ->> 'email', ''))
in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com')
);
create table if not exists public.payment_details (
id boolean primary key default true check (id),
account_name text not null default '',
account_number text not null default '',
payment_phone text not null default ''
);
alter table public.payment_details enable row level security;
grant select, insert, update, delete on public.payment_details to authenticated;
drop policy if exists "No Limits admins manage payment details" on public.payment_details;
create policy "No Limits admins manage payment details" on public.payment_details
for all to authenticated
using (lower(coalesce(auth.jwt() ->> 'email', '')) in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com'))
with check (lower(coalesce(auth.jwt() ->> 'email', '')) in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com'));
alter table public.bookings add column if not exists contact_email text not null default '';
alter table public.bookings add column if not exists event_time time not null default '00:00';
alter table public.bookings add column if not exists client_upload_key text not null default '';
alter table public.bookings add column if not exists payment_slip_path text;
alter table public.bookings add column if not exists cancellation_status text not null default 'none' check (cancellation_status in ('none', 'requested', 'approved', 'declined'));
alter table public.bookings add column if not exists cancellation_requested_at timestamptz;
alter table public.bookings add column if not exists cancellation_reason text;
revoke all on public.bookings from anon;
grant insert on public.bookings to anon;
grant select (id) on public.bookings to anon;
grant update (payment_slip_path) on public.bookings to anon;
drop policy if exists "Public clients create unpaid bookings" on public.bookings;
create policy "Public clients create unpaid bookings" on public.bookings
for insert to anon with check (
paid = false and cancellation_status = 'none' and payment_slip_path is null
and id ~* '^[0-9a-f-]{36}$' and client_upload_key ~* '^[0-9a-f-]{36}$'
and client is not null and length(trim(client)) between 1 and 80
and phone is not null and length(trim(phone)) between 3 and 30
and event_date >= (now() at time zone 'Indian/Maldives')::date
and contact_email ~* '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$'
and (
(service = 'Small Sound System' and amount = 1500) or
(service = 'Medium Sound System' and amount = 2500) or
(service = 'Big Sound System' and amount = 3000) or
(service = 'Small Lighting Setup' and amount = 2500) or
(service = 'Medium Lighting Setup' and amount = 3000) or
(service = 'Big Lighting Setup' and amount = 3500)
)
);
drop policy if exists "Public clients attach payment slips" on public.bookings;
create policy "Public clients attach payment slips" on public.bookings
for update to anon using (paid = false and cancellation_status in ('none', 'requested'))
with check (paid = false and payment_slip_path like id || '/' || client_upload_key || '/%' and cancellation_status in ('none', 'requested'));
create or replace function public.request_booking_cancellation(p_invoice_id text, p_email text, p_reason text default '')
returns boolean language plpgsql security definer set search_path = public, pg_temp as $$
begin
update public.bookings
set cancellation_status = 'requested',
cancellation_requested_at = now(),
cancellation_reason = left(coalesce(trim(p_reason), ''), 500)
where id = p_invoice_id
and lower(contact_email) = lower(trim(p_email))
and cancellation_status in ('none', 'declined');
return found;
end;
$$;
revoke all on function public.request_booking_cancellation(text, text, text) from public;
grant execute on function public.request_booking_cancellation(text, text, text) to anon, authenticated;
revoke all on public.payment_details from anon;
grant select on public.payment_details to anon;
drop policy if exists "Clients can view payment details" on public.payment_details;
create policy "Clients can view payment details" on public.payment_details for select to anon using (true);
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values ('payment-slips', 'payment-slips', false, 10485760, array['application/pdf', 'image/jpeg', 'image/png'])
on conflict (id) do update set public = false, file_size_limit = 10485760, allowed_mime_types = excluded.allowed_mime_types;
drop policy if exists "Clients upload payment slips" on storage.objects;
create policy "Clients upload payment slips" on storage.objects
for insert to anon with check (
bucket_id = 'payment-slips' and name ~* '^[0-9a-f-]{36}/[0-9a-f-]{36}/[^/]+$'
);
drop policy if exists "No Limits admins read payment slips" on storage.objects;
create policy "No Limits admins read payment slips" on storage.objects
for select to authenticated using (
bucket_id = 'payment-slips'
and lower(coalesce(auth.jwt() ->> 'email', '')) in ('n4vaaal@gmail.com', 'nolimits1240@gmail.com')
);
do $$
begin
if not exists (
select 1 from pg_publication_tables
where pubname = 'supabase_realtime' and schemaname = 'public' and tablename = 'bookings'
) then
alter publication supabase_realtime add table public.bookings;
end if;
end;
$$;
notify pgrst, 'reload schema';
If Supabase still reports a missing column after running the SQL, refresh its API schema cache with NOTIFY pgrst, 'reload schema'; in the SQL Editor, wait a few seconds, then retry the booking.
The project URL is already configured below. Copy the project's public anon/publishable key into SUPABASE_ANON_KEY in the page script. Never use a service-role key in this HTML. Deploy the updated page over HTTPS and set the matching Supabase Auth site URL/redirect URLs.