feat(server): deliver push without a Firebase project
Every notification depended on Firebase Cloud Messaging, so a missing Firebase project, which is the current state, meant no notification could be delivered on any platform. Web Push and token-based Apple Push are now first class transports alongside FCM, chosen per registered device, and a scheduled Cloudflare Worker drain retries an outbox so a provider outage delays rather than drops a message.
This commit is contained in:
parent
5aa268970a
commit
bb0e54dcee
13 changed files with 1412 additions and 41 deletions
148
server/supabase/migrations/20260913000033_team_activities.sql
Normal file
148
server/supabase/migrations/20260913000033_team_activities.sql
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
-- ============================================================================
|
||||
-- Team activity feed
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Adds a team-scoped activity/comment stream so members can see shared notes
|
||||
-- and system events (member changes, invites, shared meetings/documents).
|
||||
--
|
||||
-- Follows the locked RPC write model established in
|
||||
-- 20260821000008_atomic_team_security.sql: team tables have no direct
|
||||
-- INSERT/UPDATE/DELETE policies, so all writes go through SECURITY DEFINER
|
||||
-- RPCs. Reads are RLS-filtered to team members via public.user_team_ids().
|
||||
--
|
||||
-- Also registers the team tables (and this one) with the realtime publication.
|
||||
-- Previously teams/team_members/team_invites were never added, so the mobile
|
||||
-- team Realtime subscriptions could not receive postgres_changes events.
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.team_activities (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id uuid NOT NULL REFERENCES public.teams(id) ON DELETE CASCADE,
|
||||
actor_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
kind text NOT NULL CHECK (
|
||||
kind IN (
|
||||
'note',
|
||||
'member_joined',
|
||||
'member_left',
|
||||
'invite_created',
|
||||
'meeting_shared',
|
||||
'document_shared'
|
||||
)
|
||||
),
|
||||
body text CHECK (body IS NULL OR char_length(body) <= 2000),
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_activities_team_created
|
||||
ON public.team_activities (team_id, created_at DESC);
|
||||
|
||||
ALTER TABLE public.team_activities ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS team_activities_read_member ON public.team_activities;
|
||||
CREATE POLICY team_activities_read_member ON public.team_activities
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (team_id IN (SELECT public.user_team_ids(auth.uid())));
|
||||
|
||||
-- Writes are RPC-only: no INSERT/UPDATE/DELETE policies exist on purpose.
|
||||
GRANT SELECT ON public.team_activities TO authenticated;
|
||||
REVOKE INSERT, UPDATE, DELETE ON public.team_activities FROM PUBLIC, anon, authenticated;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_team_activity(
|
||||
p_team_id uuid,
|
||||
p_kind text,
|
||||
p_body text,
|
||||
p_metadata jsonb DEFAULT '{}'::jsonb
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
member_role text;
|
||||
normalized_body text;
|
||||
new_id uuid;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF p_kind IS NULL OR p_kind NOT IN (
|
||||
'note',
|
||||
'member_joined',
|
||||
'member_left',
|
||||
'invite_created',
|
||||
'meeting_shared',
|
||||
'document_shared'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'invalid_activity_kind' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
normalized_body := nullif(btrim(coalesce(p_body, '')), '');
|
||||
IF normalized_body IS NOT NULL AND char_length(normalized_body) > 2000 THEN
|
||||
RAISE EXCEPTION 'activity_body_too_long' USING ERRCODE = '22001';
|
||||
END IF;
|
||||
IF normalized_body IS NULL AND p_kind = 'note' THEN
|
||||
RAISE EXCEPTION 'activity_body_required' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(p_team_id::text, 73053));
|
||||
SELECT role INTO member_role FROM public.team_members
|
||||
WHERE team_id = p_team_id AND user_id = current_user_id;
|
||||
IF member_role IS NULL THEN
|
||||
RAISE EXCEPTION 'team_member_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
INSERT INTO public.team_activities (team_id, actor_id, kind, body, metadata)
|
||||
VALUES (
|
||||
p_team_id,
|
||||
current_user_id,
|
||||
p_kind,
|
||||
normalized_body,
|
||||
coalesce(p_metadata, '{}'::jsonb)
|
||||
)
|
||||
RETURNING id INTO new_id;
|
||||
RETURN jsonb_build_object(
|
||||
'id', new_id,
|
||||
'team_id', p_team_id,
|
||||
'actor_id', current_user_id,
|
||||
'kind', p_kind,
|
||||
'body', normalized_body,
|
||||
'created_at', now()
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.create_team_activity(uuid, text, text, jsonb) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.create_team_activity(uuid, text, text, jsonb) TO authenticated;
|
||||
|
||||
-- Realtime: register team tables + activities. ALTER PUBLICATION ADD TABLE has
|
||||
-- no IF NOT EXISTS, so probe pg_publication_tables first.
|
||||
DO $$
|
||||
DECLARE
|
||||
t text;
|
||||
BEGIN
|
||||
FOR t IN
|
||||
SELECT unnest(ARRAY[
|
||||
'team_activities',
|
||||
'teams',
|
||||
'team_members',
|
||||
'team_invites'
|
||||
])
|
||||
LOOP
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_publication_tables
|
||||
WHERE pubname = 'supabase_realtime'
|
||||
AND schemaname = 'public'
|
||||
AND tablename = t
|
||||
) THEN
|
||||
EXECUTE format(
|
||||
'ALTER PUBLICATION supabase_realtime ADD TABLE public.%I',
|
||||
t
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue