All blocks
Scheduling
Reminders
Recurrence rules and reminders. Expansion follows RFC 5545 in TypeScript. Delivery is a job that records reminder.sent once per occurrence.
npx better-supabase sql add remindersimport { defineConfig } from "better-supabase/config";
export default defineConfig({
sql: {
modules: {
// Required by reminders; sql add installs them too.
"updated-at": {},
tenant: {},
jobs: {},
access: {},
// Modes: "managed", "adopt", "custom". The default is "managed".
reminders: { mode: "managed" },
},
},
});npx better-supabase sql add reminders writes these files into your declarative schema with the default layout. See SQL modules for the migration step and the layout options.
supabase/schemas/900_better_supabase_01_updated_at.sqlschemaupdated-at100 linessupabase/better-supabase-data/900_better_supabase_01_updated_at.sqldataupdated-at12 linessupabase/schemas/900_better_supabase_04_tenant.sqlschematenant156 linessupabase/better-supabase-data/900_better_supabase_04_tenant.sqldatatenant12 linessupabase/schemas/900_better_supabase_07_jobs.sqlschemajobs534 linessupabase/better-supabase-data/900_better_supabase_07_jobs.sqldatajobs17 linessupabase/schemas/900_better_supabase_19_access.sqlschemaaccess375 linessupabase/better-supabase-data/900_better_supabase_19_access.sqldataaccess12 linessupabase/schemas/900_better_supabase_63_reminders.sqlschema182 linessupabase/better-supabase-data/900_better_supabase_63_reminders.sqldata12 lines
supabase/schemas/900_better_supabase_63_reminders.sql
-- better-supabase module: reminders
-- @bs-module reminders@1 managed
-- Recurrence rules and reminders. Expansion follows RFC 5545 in TypeScript. Delivery is a job that records reminder.sent once per occurrence.
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
create or replace function better_supabase.actor_may_write()
returns boolean
language sql
stable
security invoker
set search_path = ''
as $$
select case
when auth.jwt() is null then true
when coalesce(auth.jwt() -> 'act' ->> 'read_only', '') = 'false' then true
when auth.jwt() -> 'act' ->> 'kind' = 'agent'
then coalesce(auth.jwt() -> 'act' ->> 'read_only', '') <> 'true'
when auth.jwt() -> 'act' ->> 'kind' in ('support', 'impersonation') then false
when auth.jwt() -> 'act' ->> 'session_id' is not null
and auth.jwt() -> 'act' ->> 'kind' is null then false
else true
end
$$;
revoke execute on function better_supabase.actor_may_write() from public;
grant execute on function better_supabase.actor_may_write() to anon, authenticated, service_role;
create or replace function better_supabase.raise_actor_readonly()
returns void
language plpgsql
volatile
set search_path = ''
as $$
begin
raise exception 'This session is read-only' using errcode = '42501', hint = 'ACTOR_READ_ONLY';
end;
$$;
revoke execute on function better_supabase.raise_actor_readonly() from public;
grant execute on function better_supabase.raise_actor_readonly() to anon, authenticated, service_role;
create schema if not exists better_supabase;
grant usage on schema better_supabase to anon, authenticated, service_role;
create table if not exists "better_supabase"."recurrence_rules" (
"id" uuid primary key default gen_random_uuid(),
"tenant_id" uuid not null,
"subject_type" text not null,
"subject_id" text not null,
"dtstart" timestamptz not null,
"tzid" text not null default 'UTC',
"rrule" text,
unique ("tenant_id", "subject_type", "subject_id")
);
alter table "better_supabase"."recurrence_rules" enable row level security;
revoke all on "better_supabase"."recurrence_rules" from anon, authenticated;
grant select on "better_supabase"."recurrence_rules" to authenticated;
grant all on "better_supabase"."recurrence_rules" to service_role;
create table if not exists "better_supabase"."reminders" (
"id" uuid primary key default gen_random_uuid(),
"tenant_id" uuid not null,
"subject_type" text not null,
"subject_id" text not null,
"user_id" uuid,
"method" text not null default 'notification',
"offset_minutes" integer not null default 0
);
alter table "better_supabase"."reminders" enable row level security;
revoke all on "better_supabase"."reminders" from anon, authenticated;
grant select on "better_supabase"."reminders" to authenticated;
grant all on "better_supabase"."reminders" to service_role;
create table if not exists "better_supabase"."reminder_deliveries" (
"id" uuid primary key default gen_random_uuid(),
"reminder_id" uuid not null references "better_supabase"."reminders" ("id") on delete cascade,
"occurrence_at" timestamptz not null,
"sent_at" timestamptz,
unique ("reminder_id", "occurrence_at")
);
alter table "better_supabase"."reminder_deliveries" enable row level security;
revoke all on "better_supabase"."reminder_deliveries" from anon, authenticated;
grant select on "better_supabase"."reminder_deliveries" to authenticated;
grant all on "better_supabase"."reminder_deliveries" to service_role;
create or replace function "better_supabase"."save_reminder"(tenant uuid, subject_type text, subject_id text, offset_minutes integer)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_id uuid;
begin
if not (better_supabase.actor_may_write()) then
raise exception 'This session is read-only' using errcode = '42501', hint = 'ACTOR_READ_ONLY';
end if;
if not (auth.role() = 'service_role' or coalesce(better_supabase.can('tenant', save_reminder.tenant, 'reminders.manage'), false)) then
raise exception 'not allowed to manage reminders' using errcode = '42501', hint = 'REMINDER_FORBIDDEN';
end if;
insert into "better_supabase"."reminders" ("tenant_id", "subject_type", "subject_id", "user_id", "offset_minutes")
values (save_reminder.tenant, save_reminder.subject_type, save_reminder.subject_id, auth.uid(), coalesce(save_reminder.offset_minutes, 0))
returning "id" into v_id;
return jsonb_build_object('id', v_id);
end;
$$;
revoke execute on function "better_supabase"."save_reminder"(uuid, text, text, integer) from public, anon;
grant execute on function "better_supabase"."save_reminder"(uuid, text, text, integer) to authenticated, service_role;
create or replace function "better_supabase"."mark_reminder_sent"(reminder uuid, occurrence_at timestamptz)
returns boolean
language plpgsql
security definer
set search_path = ''
as $$
#variable_conflict use_column
declare
v_tenant uuid;
v_subject_type text;
v_subject_id text;
v_user uuid;
begin
select "tenant_id", "subject_type", "subject_id", "user_id"
into v_tenant, v_subject_type, v_subject_id, v_user
from "better_supabase"."reminders" where "id" = mark_reminder_sent.reminder;
if not found then
raise exception 'reminder not found' using errcode = 'P0002', hint = 'REMINDER_NOT_FOUND';
end if;
insert into "better_supabase"."reminder_deliveries" ("reminder_id", "occurrence_at", "sent_at")
values (mark_reminder_sent.reminder, mark_reminder_sent.occurrence_at, now())
on conflict ("reminder_id", "occurrence_at") do nothing;
-- A second delivery of the same occurrence records nothing.
if not found then
return false;
end if;
null;
return true;
end;
$$;
revoke execute on function "better_supabase"."mark_reminder_sent"(uuid, timestamptz) from public, anon, authenticated;
grant execute on function "better_supabase"."mark_reminder_sent"(uuid, timestamptz) to service_role;
create or replace function "better_supabase"."list_reminder_schedule"()
returns table (
reminder uuid,
dtstart timestamptz,
tzid text,
rrule text,
offset_minutes integer,
tenant uuid,
subject_type text,
subject_id text
)
language sql
stable
security definer
set search_path = ''
as $$
select r."id",
u."dtstart",
u."tzid",
u."rrule",
r."offset_minutes",
r."tenant_id",
r."subject_type",
r."subject_id"
from "better_supabase"."reminders" r
join "better_supabase"."recurrence_rules" u
on u."tenant_id" = r."tenant_id"
and u."subject_type" = r."subject_type"
and u."subject_id" = r."subject_id";
$$;
revoke execute on function "better_supabase"."list_reminder_schedule"() from public, anon, authenticated;
grant execute on function "better_supabase"."list_reminder_schedule"() to service_role;
create schema if not exists better_supabase;
create table if not exists better_supabase.modules (
name text primary key,
version integer not null,
mode text not null,
installed_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table better_supabase.modules enable row level security;
revoke all on better_supabase.modules from anon, authenticated;
grant select on better_supabase.modules to service_role;
supabase/better-supabase-data/900_better_supabase_63_reminders.sql
-- better-supabase module: reminders
-- @bs-module-data reminders
-- Rows and settings a schema diff doesn't capture. Run `better-supabase sql data`
-- after the schema migration to put them in a migration.
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
insert into better_supabase.modules (name, version, mode)
values ('reminders', 1, 'managed')
on conflict (name) do update
set version = excluded.version, mode = excluded.mode, updated_at = now();
This block has no React entry yet. Its TypeScript side is in better-supabase/blocks/reminders, which runs on the server.
With the outbox installed, the module writes these events in the same transaction as the change.
- reminder.sent