monacousa-portal/ARCHITECTURE.md

147 KiB

Monaco USA Portal 2026 - Complete Rebuild

Project Overview

Rebuild the Monaco USA member portal from scratch in monacousa-portal-2026/ with modern architecture, beautiful UI, and improved functionality.


DETAILED FEATURE SPECIFICATIONS

1. MEMBER SYSTEM (Detailed)

1.1 Member ID Format

  • Format: MUSA-XXXX (sequential 4-digit number)
  • Examples: MUSA-0001, MUSA-0042, MUSA-1234
  • Auto-generated on member creation
  • Immutable once assigned
  • Unique constraint in database

1.2 Membership Statuses (Admin-Configurable)

Admin can create, edit, and delete statuses via Settings.

Default Statuses (seeded on first run):

Status Color Description Is Default
pending Yellow New member, awaiting dues payment Yes (for new signups)
active Green Dues paid, full access No
inactive Gray Lapsed membership or suspended No
expired Red Membership terminated No

Status Configuration Table:

CREATE TABLE public.membership_statuses (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL,
  color TEXT NOT NULL DEFAULT '#6b7280',  -- Tailwind gray-500
  description TEXT,
  is_default BOOLEAN DEFAULT FALSE,       -- Used for new signups
  sort_order INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

1.3 Roles/Tiers

Fixed 3-tier system (not configurable):

Role Access Level Capabilities
member Basic View own profile, events, pay dues
board Elevated + Member directory, record payments, manage events
admin Full + User management, system settings, all data

1.4 Required Member Fields

All fields marked as required during signup:

Field Type Validation Notes
first_name Text Min 2 chars Required
last_name Text Min 2 chars Required
email Email Valid email format Required, unique
phone Text International format Required
date_of_birth Date Must be 18+ years old Required
address Text Min 10 chars Required
nationality Array At least 1 country Required, multiple allowed

1.5 Optional Member Fields

Field Type Notes
avatar_url Text Supabase Storage path
membership_type_id UUID Links to membership_types table
notes Text Admin-only notes about member

1.6 Nationality Handling

  • Multiple nationalities allowed
  • Stored as PostgreSQL TEXT[] array
  • Uses ISO 3166-1 alpha-2 country codes: ['FR', 'US', 'MC']
  • UI shows country flags + names
  • Searchable/filterable in directory

1.7 Profile Features

  • Profile photo: Upload via Supabase Storage
    • Max size: 5MB
    • Formats: JPG, PNG, WebP
    • Auto-resized to 256x256
    • Stored at: avatars/{member_id}/profile.{ext}
  • No bio field (simplified profile)
  • Members can edit: name, phone, address, nationality, photo

1.8 Member Directory

Visibility controlled by admin settings:

CREATE TABLE public.directory_settings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  field_name TEXT NOT NULL UNIQUE,
  visible_to_members BOOLEAN DEFAULT FALSE,
  visible_to_board BOOLEAN DEFAULT TRUE,
  visible_to_admin BOOLEAN DEFAULT TRUE,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Default visibility settings
INSERT INTO directory_settings (field_name, visible_to_members, visible_to_board) VALUES
  ('first_name', true, true),
  ('last_name', true, true),
  ('avatar_url', true, true),
  ('nationality', true, true),
  ('email', false, true),
  ('phone', false, true),
  ('address', false, true),
  ('date_of_birth', false, true),
  ('member_since', true, true),
  ('membership_status', false, true);

1.9 Member Signup Flow

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  /signup    │────▶│ Create Auth  │────▶│ Email Verify│
│  Form       │     │ User + Member│     │ Link Sent   │
└─────────────┘     └──────────────┘     └─────────────┘
                           │
                           ▼
                    ┌──────────────┐     ┌─────────────┐
                    │ Status =     │────▶│ Wait for    │
                    │ 'pending'    │     │ Dues Payment│
                    └──────────────┘     └─────────────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │ Board/Admin │
                                        │ Records Dues│
                                        └─────────────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │ Status =    │
                                        │ 'active'    │
                                        └─────────────┘

Key Points:

  • Email verification required
  • Status starts as pending
  • Member gains active status ONLY when first dues payment recorded
  • Pending members can log in but see limited dashboard

1.10 Admin Member Management

Two ways to add members:

Option A: Direct Add

  1. Admin fills out member form
  2. Admin sets temporary password OR sends password setup email
  3. Member record created with chosen status
  4. Member can log in immediately

Option B: Invite

  1. Admin enters email + basic info
  2. System sends invitation email with signup link
  3. Invitee completes signup form
  4. Status set based on invite settings

1.11 Membership Types (Admin-Configurable)

Admin can create membership tiers with different pricing:

CREATE TABLE public.membership_types (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE,           -- 'regular', 'student', 'senior'
  display_name TEXT NOT NULL,          -- 'Regular Member', 'Student'
  annual_dues DECIMAL(10,2) NOT NULL,  -- 50.00, 25.00, etc.
  description TEXT,
  is_default BOOLEAN DEFAULT FALSE,    -- Default for new signups
  is_active BOOLEAN DEFAULT TRUE,      -- Can be assigned
  sort_order INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Default membership types
INSERT INTO membership_types (name, display_name, annual_dues, is_default) VALUES
  ('regular', 'Regular Member', 50.00, true),
  ('student', 'Student', 25.00, false),
  ('senior', 'Senior (65+)', 35.00, false),
  ('family', 'Family', 75.00, false),
  ('honorary', 'Honorary Member', 0.00, false);

1.12 Complete Member Schema

CREATE TABLE public.members (
  -- Identity
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  member_id TEXT UNIQUE NOT NULL,        -- MUSA-0001 format (auto-generated)

  -- Required Personal Info
  first_name TEXT NOT NULL,
  last_name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  phone TEXT NOT NULL,
  date_of_birth DATE NOT NULL,
  address TEXT NOT NULL,
  nationality TEXT[] NOT NULL DEFAULT '{}',

  -- Membership
  role TEXT NOT NULL DEFAULT 'member'
    CHECK (role IN ('member', 'board', 'admin')),
  membership_status_id UUID REFERENCES public.membership_statuses(id),
  membership_type_id UUID REFERENCES public.membership_types(id),
  member_since DATE DEFAULT CURRENT_DATE,

  -- Profile
  avatar_url TEXT,

  -- Admin
  notes TEXT,                            -- Admin-only notes

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Auto-generate member_id trigger
CREATE OR REPLACE FUNCTION generate_member_id()
RETURNS TRIGGER AS $$
DECLARE
  next_num INTEGER;
BEGIN
  SELECT COALESCE(MAX(CAST(SUBSTRING(member_id FROM 6) AS INTEGER)), 0) + 1
  INTO next_num
  FROM public.members;

  NEW.member_id := 'MUSA-' || LPAD(next_num::TEXT, 4, '0');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_member_id
  BEFORE INSERT ON public.members
  FOR EACH ROW
  WHEN (NEW.member_id IS NULL)
  EXECUTE FUNCTION generate_member_id();

2. DUES/PAYMENTS SYSTEM (Detailed)

2.1 Dues Cycle

  • Due date calculation: Payment date + 365 days
  • Example: Payment on Jan 15, 2026 → Due Jan 15, 2027
  • No proration: Full annual dues regardless of join date

2.2 Payment Methods

Bank transfer only (no online payments):

  • IBAN tracking
  • Reference number for matching
  • Manual recording by Board/Admin

2.3 Payment Recording

Who can record payments:

  • Board members
  • Admins

Standard payment data tracked:

Field Type Required Description
member_id UUID Yes Which member
amount Decimal Yes Payment amount (€)
payment_date Date Yes When payment was made
due_date Date Yes When this payment period ends (auto-calculated)
reference Text No Bank transfer reference
payment_method Text Yes Always 'bank_transfer' for now
recorded_by UUID Yes Board/Admin who recorded
notes Text No Optional notes

2.4 Dues Settings (Admin-Configurable)

CREATE TABLE public.dues_settings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  setting_key TEXT UNIQUE NOT NULL,
  setting_value TEXT NOT NULL,
  description TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  updated_by UUID REFERENCES public.members(id)
);

-- Default settings
INSERT INTO dues_settings (setting_key, setting_value, description) VALUES
  ('reminder_days_before', '30,7', 'Days before due date to send reminders (comma-separated)'),
  ('grace_period_days', '30', 'Days after due date before auto-inactive'),
  ('overdue_reminder_interval', '14', 'Days between overdue reminder emails'),
  ('payment_iban', 'MC58 1756 9000 0104 0050 1001 860', 'IBAN for dues payment'),
  ('payment_account_holder', 'ASSOCIATION MONACO USA', 'Account holder name'),
  ('payment_instructions', 'Please include your Member ID in the reference', 'Payment instructions');

2.5 Automatic Reminders

Reminder Schedule (configurable via settings):

  1. 30 days before due date: "Your dues are coming up"
  2. 7 days before due date: "Reminder: dues due in 1 week"
  3. On due date: "Your dues are now due"
  4. Every 14 days overdue: "Your dues are overdue" (until grace period ends)

Email Content Includes:

  • Member name
  • Amount due (from membership_type)
  • Due date
  • IBAN and account holder
  • Payment reference suggestion (Member ID)
  • Link to portal

Technical Implementation:

  • Supabase Edge Function runs daily
  • Checks all members for reminder triggers
  • Logs sent emails in email_logs table
  • Respects settings for intervals

2.6 Overdue Handling

Grace Period Flow:

Due Date Passed
      │
      ▼
┌─────────────────────────────────────────┐
│  GRACE PERIOD (configurable, default 30 days)  │
│  - Status remains 'active'              │
│  - Overdue reminders sent               │
│  - Flagged in dashboard                 │
└─────────────────────────────────────────┘
      │
      ▼ (grace period ends)
┌─────────────────────────────────────────┐
│  AUTO STATUS CHANGE                      │
│  - Status → 'inactive'                  │
│  - Final notification email             │
│  - Member loses active access           │
└─────────────────────────────────────────┘

Supabase Edge Function for Auto-Update:

// Runs daily via cron
async function updateOverdueMembers() {
  const gracePeriodDays = await getSetting('grace_period_days');
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - gracePeriodDays);

  // Find members past grace period
  const { data: overdueMembers } = await supabase
    .from('members_with_dues')
    .select('*')
    .eq('membership_status', 'active')
    .lt('current_due_date', cutoffDate.toISOString());

  // Update each to inactive
  for (const member of overdueMembers) {
    await supabase
      .from('members')
      .update({ membership_status_id: inactiveStatusId })
      .eq('id', member.id);

    // Send final notification
    await sendEmail(member.email, 'membership_lapsed', { ... });
  }
}

2.7 Payment History (Member Visible)

Members can see their complete payment history:

Display includes:

  • Payment date
  • Amount paid
  • Due date (period covered)
  • Reference number
  • Payment method

Members CANNOT see:

  • Who recorded the payment
  • Internal notes
  • Other members' payments

2.8 Dues Dashboard (Board/Admin)

Overview Stats:

  • Total members with current dues
  • Members with dues due soon (next 30 days)
  • Overdue members count
  • Total collected this year

Filterable Member List:

  • Filter by: status (current, due soon, overdue, never paid)
  • Sort by: due date, days overdue, member name
  • Quick actions: Record payment, Send reminder

Individual Member View:

  • Full payment history
  • Current dues status
  • Quick record payment form
  • Send manual reminder button

2.9 Complete Dues Schema

-- Dues payments table
CREATE TABLE public.dues_payments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,

  amount DECIMAL(10,2) NOT NULL,
  currency TEXT DEFAULT 'EUR',
  payment_date DATE NOT NULL,
  due_date DATE NOT NULL,                   -- Calculated: payment_date + 1 year
  payment_method TEXT DEFAULT 'bank_transfer',
  reference TEXT,                           -- Bank transfer reference
  notes TEXT,                               -- Internal notes

  recorded_by UUID NOT NULL REFERENCES public.members(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Trigger to auto-calculate due_date
CREATE OR REPLACE FUNCTION calculate_due_date()
RETURNS TRIGGER AS $$
BEGIN
  NEW.due_date := NEW.payment_date + INTERVAL '1 year';
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_due_date
  BEFORE INSERT ON public.dues_payments
  FOR EACH ROW
  WHEN (NEW.due_date IS NULL)
  EXECUTE FUNCTION calculate_due_date();

-- After payment: update member status to active
CREATE OR REPLACE FUNCTION update_member_status_on_payment()
RETURNS TRIGGER AS $$
DECLARE
  active_status_id UUID;
BEGIN
  -- Get active status ID
  SELECT id INTO active_status_id
  FROM public.membership_statuses
  WHERE name = 'active';

  -- Update member status
  UPDATE public.members
  SET membership_status_id = active_status_id,
      updated_at = NOW()
  WHERE id = NEW.member_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER activate_member_on_payment
  AFTER INSERT ON public.dues_payments
  FOR EACH ROW
  EXECUTE FUNCTION update_member_status_on_payment();

-- Computed view for dues status
CREATE VIEW public.members_with_dues AS
SELECT
  m.*,
  ms.name as status_name,
  ms.display_name as status_display_name,
  ms.color as status_color,
  mt.display_name as membership_type_name,
  mt.annual_dues,
  dp.last_payment_date,
  dp.current_due_date,
  CASE
    WHEN dp.current_due_date IS NULL THEN 'never_paid'
    WHEN dp.current_due_date < CURRENT_DATE THEN 'overdue'
    WHEN dp.current_due_date < CURRENT_DATE + INTERVAL '30 days' THEN 'due_soon'
    ELSE 'current'
  END as dues_status,
  CASE
    WHEN dp.current_due_date < CURRENT_DATE
    THEN (CURRENT_DATE - dp.current_due_date)::INTEGER
    ELSE NULL
  END as days_overdue,
  CASE
    WHEN dp.current_due_date >= CURRENT_DATE
    THEN (dp.current_due_date - CURRENT_DATE)::INTEGER
    ELSE NULL
  END as days_until_due
FROM public.members m
LEFT JOIN public.membership_statuses ms ON m.membership_status_id = ms.id
LEFT JOIN public.membership_types mt ON m.membership_type_id = mt.id
LEFT JOIN LATERAL (
  SELECT
    payment_date as last_payment_date,
    due_date as current_due_date
  FROM public.dues_payments
  WHERE member_id = m.id
  ORDER BY due_date DESC
  LIMIT 1
) dp ON true;

2.10 Email Templates for Dues

Types:

  1. dues_reminder - Upcoming dues reminder
  2. dues_due_today - Dues due today
  3. dues_overdue - Overdue reminder
  4. dues_lapsed - Membership lapsed (grace period ended)
  5. dues_received - Payment confirmation

Template Variables:

  • {{member_name}} - Full name
  • {{member_id}} - MUSA-XXXX
  • {{amount}} - Due amount
  • {{due_date}} - Formatted date
  • {{days_until_due}} or {{days_overdue}}
  • {{iban}} - Payment IBAN
  • {{account_holder}} - Account name
  • {{portal_link}} - Link to portal

3. EVENTS SYSTEM (Detailed)

3.1 Event Types (Admin-Configurable)

CREATE TABLE public.event_types (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL,
  color TEXT NOT NULL DEFAULT '#3b82f6',  -- Tailwind blue-500
  icon TEXT,                               -- Lucide icon name
  description TEXT,
  is_active BOOLEAN DEFAULT TRUE,
  sort_order INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Default event types
INSERT INTO event_types (name, display_name, color, icon) VALUES
  ('social', 'Social Event', '#10b981', 'party-popper'),
  ('meeting', 'Meeting', '#6366f1', 'users'),
  ('fundraiser', 'Fundraiser', '#f59e0b', 'heart-handshake'),
  ('workshop', 'Workshop', '#8b5cf6', 'graduation-cap'),
  ('gala', 'Gala/Formal', '#ec4899', 'sparkles'),
  ('other', 'Other', '#6b7280', 'calendar');

3.2 Event Visibility

Visibility Options:

Level Who Can See Description
public Anyone Visible on public events page (no login)
members All logged-in members Default for most events
board Board + Admin only Board meetings, internal events
admin Admin only Administrative events

3.3 Event Pricing

Pricing Model:

  • Each event can be free or paid
  • Paid events have member price and non-member price
  • Member pricing determined by membership_type_id (if tiered pricing enabled)
  • Non-members pay non-member price always

Pricing Fields:

is_paid BOOLEAN DEFAULT FALSE,
member_price DECIMAL(10,2) DEFAULT 0,
non_member_price DECIMAL(10,2) DEFAULT 0,
pricing_notes TEXT  -- "Includes dinner and drinks"

3.4 Guest/+1 Handling

Per-Event Configuration:

  • max_guests_per_member - 0, 1, 2, 3, or unlimited
  • Each RSVP tracks guest count and guest names
  • Guests count toward total capacity
  • Non-members can bring guests too (if enabled)

3.5 Non-Member (Public) RSVP

Flow for public events:

┌─────────────────┐     ┌──────────────────┐
│ Public Events   │────▶│ Event Detail     │
│ Page (no login) │     │ (public visible) │
└─────────────────┘     └──────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │ RSVP Form        │
                        │ (no account)     │
                        │ - Name           │
                        │ - Email          │
                        │ - Phone          │
                        │ - Guest count    │
                        │ - Guest names    │
                        └──────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │ Payment Info     │
                        │ (if paid event)  │
                        │ - IBAN shown     │
                        │ - Reference #    │
                        └──────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │ RSVP Confirmed   │
                        │ (pending payment)│
                        │ Email sent       │
                        └──────────────────┘

Non-Member RSVP Table:

CREATE TABLE public.event_rsvps_public (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,

  -- Contact info (required)
  full_name TEXT NOT NULL,
  email TEXT NOT NULL,
  phone TEXT,

  -- RSVP details
  status TEXT NOT NULL DEFAULT 'confirmed'
    CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')),
  guest_count INTEGER DEFAULT 0,
  guest_names TEXT[],

  -- Payment (for paid events)
  payment_status TEXT DEFAULT 'not_required'
    CHECK (payment_status IN ('not_required', 'pending', 'paid')),
  payment_reference TEXT,
  payment_amount DECIMAL(10,2),

  -- Attendance
  attended BOOLEAN DEFAULT FALSE,

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(event_id, email)  -- One RSVP per email per event
);

3.6 RSVP Status Options

For Members and Non-Members:

Status Description
confirmed Attending the event
declined Not attending
maybe Tentative/undecided
waitlist Event full, on waitlist
cancelled Cancelled RSVP

3.7 Capacity & Waitlist

Capacity Management:

  • max_attendees - Total spots (null = unlimited)
  • Includes members + guests + non-members + their guests
  • When full, new RSVPs go to waitlist

Auto-Promote Waitlist:

// Trigger when RSVP is cancelled or declined
async function promoteFromWaitlist(eventId: string) {
  // Get event capacity
  const event = await getEvent(eventId);
  const currentCount = await getCurrentAttendeeCount(eventId);

  if (event.max_attendees && currentCount >= event.max_attendees) {
    return; // Still full
  }

  // Get oldest waitlist entry
  const waitlisted = await supabase
    .from('event_rsvps')
    .select('*')
    .eq('event_id', eventId)
    .eq('status', 'waitlist')
    .order('created_at', { ascending: true })
    .limit(1)
    .single();

  if (waitlisted) {
    // Promote to confirmed
    await supabase
      .from('event_rsvps')
      .update({ status: 'confirmed' })
      .eq('id', waitlisted.id);

    // Send notification email
    await sendEmail(waitlisted.member.email, 'waitlist_promoted', {
      event_title: event.title,
      event_date: event.start_datetime
    });
  }
}

3.8 Attendance Tracking

Check-in System:

  • Board/Admin can mark attendance after event
  • Checkbox per RSVP: attended yes/no
  • Track attendance rate per event
  • Member attendance history viewable
-- Add to RSVPs
attended BOOLEAN DEFAULT FALSE,
checked_in_at TIMESTAMPTZ,
checked_in_by UUID REFERENCES public.members(id)

3.9 Calendar Views

Available Views:

  1. Month - Traditional calendar grid
  2. Week - Weekly schedule view
  3. Day - Single day detailed view
  4. List - Upcoming events list

Using FullCalendar (SvelteKit compatible):

import Calendar from '@event-calendar/core';
import TimeGrid from '@event-calendar/time-grid';
import DayGrid from '@event-calendar/day-grid';
import List from '@event-calendar/list';

3.10 Event Schema

CREATE TABLE public.events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Basic Info
  title TEXT NOT NULL,
  description TEXT,
  event_type_id UUID REFERENCES public.event_types(id),

  -- Date/Time
  start_datetime TIMESTAMPTZ NOT NULL,
  end_datetime TIMESTAMPTZ NOT NULL,
  all_day BOOLEAN DEFAULT FALSE,
  timezone TEXT DEFAULT 'Europe/Monaco',

  -- Location
  location TEXT,
  location_url TEXT,  -- Google Maps link, etc.

  -- Capacity
  max_attendees INTEGER,  -- null = unlimited
  max_guests_per_member INTEGER DEFAULT 1,

  -- Pricing
  is_paid BOOLEAN DEFAULT FALSE,
  member_price DECIMAL(10,2) DEFAULT 0,
  non_member_price DECIMAL(10,2) DEFAULT 0,
  pricing_notes TEXT,

  -- Visibility
  visibility TEXT NOT NULL DEFAULT 'members'
    CHECK (visibility IN ('public', 'members', 'board', 'admin')),

  -- Status
  status TEXT NOT NULL DEFAULT 'published'
    CHECK (status IN ('draft', 'published', 'cancelled', 'completed')),

  -- Media
  cover_image_url TEXT,  -- Event banner/cover image

  -- Meta
  created_by UUID NOT NULL REFERENCES public.members(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Member RSVPs
CREATE TABLE public.event_rsvps (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
  member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,

  status TEXT NOT NULL DEFAULT 'confirmed'
    CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')),
  guest_count INTEGER DEFAULT 0,
  guest_names TEXT[],
  notes TEXT,

  -- Payment (for paid events)
  payment_status TEXT DEFAULT 'not_required'
    CHECK (payment_status IN ('not_required', 'pending', 'paid')),
  payment_reference TEXT,
  payment_amount DECIMAL(10,2),

  -- Attendance
  attended BOOLEAN DEFAULT FALSE,
  checked_in_at TIMESTAMPTZ,
  checked_in_by UUID REFERENCES public.members(id),

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(event_id, member_id)
);

-- View for event with counts
CREATE VIEW public.events_with_counts AS
SELECT
  e.*,
  et.display_name as event_type_name,
  et.color as event_type_color,
  et.icon as event_type_icon,
  COALESCE(member_rsvps.confirmed_count, 0) +
    COALESCE(member_rsvps.guest_count, 0) +
    COALESCE(public_rsvps.confirmed_count, 0) +
    COALESCE(public_rsvps.guest_count, 0) as total_attendees,
  COALESCE(member_rsvps.confirmed_count, 0) as member_count,
  COALESCE(public_rsvps.confirmed_count, 0) as non_member_count,
  COALESCE(member_rsvps.waitlist_count, 0) +
    COALESCE(public_rsvps.waitlist_count, 0) as waitlist_count,
  CASE
    WHEN e.max_attendees IS NULL THEN FALSE
    WHEN (COALESCE(member_rsvps.confirmed_count, 0) +
          COALESCE(member_rsvps.guest_count, 0) +
          COALESCE(public_rsvps.confirmed_count, 0) +
          COALESCE(public_rsvps.guest_count, 0)) >= e.max_attendees THEN TRUE
    ELSE FALSE
  END as is_full
FROM public.events e
LEFT JOIN public.event_types et ON e.event_type_id = et.id
LEFT JOIN LATERAL (
  SELECT
    COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count,
    COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count,
    COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count
  FROM public.event_rsvps
  WHERE event_id = e.id
) member_rsvps ON true
LEFT JOIN LATERAL (
  SELECT
    COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count,
    COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count,
    COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count
  FROM public.event_rsvps_public
  WHERE event_id = e.id
) public_rsvps ON true;

3.11 Event Permissions

Action Member Board Admin
View public events - - -
View member events
View board events -
View admin events - -
RSVP to events
Create events -
Edit own events -
Edit any event - -
Delete events - -
Manage RSVPs -
Track attendance -

3.12 Event Email Notifications

Email Types:

  1. event_created - New event announcement (for public/member events)
  2. event_reminder - Reminder before event (configurable: 1 day, 1 hour)
  3. event_updated - Event details changed
  4. event_cancelled - Event cancelled
  5. rsvp_confirmation - RSVP received
  6. waitlist_promoted - Promoted from waitlist
  7. event_payment_reminder - Payment reminder for paid events

Template Variables:

  • {{event_title}}, {{event_date}}, {{event_time}}
  • {{event_location}}, {{event_description}}
  • {{member_name}}, {{guest_count}}
  • {{payment_amount}}, {{payment_iban}}
  • {{rsvp_status}}, {{portal_link}}

4. AUTH & DASHBOARDS (Detailed)

4.1 Authentication Method

Email/Password only (no social login):

  • Standard email + password signup/login
  • Email verification required
  • Password reset via email
  • Remember me option (extended session)

4.2 Login Page Design

Branded login with:

  • Monaco USA logo
  • Association tagline
  • Login form (email, password, remember me)
  • Links: Forgot password, Sign up
  • Glass-morphism styling
  • Responsive (mobile-friendly)

4.3 Auth Flow

┌──────────────────────────────────────────────────────────────┐
│                        SIGNUP FLOW                            │
├──────────────────────────────────────────────────────────────┤
│  /signup                                                      │
│  ├── Full form (all required fields)                         │
│  ├── Supabase Auth: signUp(email, password)                  │
│  ├── Create member record (status: pending)                  │
│  ├── Send verification email                                 │
│  └── Show "Check your email" message                         │
│                                                               │
│  /auth/callback (email verification link)                    │
│  ├── Verify email token                                      │
│  ├── Update email_verified = true                            │
│  └── Redirect to /login with success message                 │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                        LOGIN FLOW                             │
├──────────────────────────────────────────────────────────────┤
│  /login                                                       │
│  ├── Email + Password form                                   │
│  ├── Supabase Auth: signInWithPassword()                     │
│  ├── Set session cookie (via Supabase SSR)                   │
│  ├── Fetch member record                                     │
│  └── Redirect to /dashboard                                  │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                     PASSWORD RESET                            │
├──────────────────────────────────────────────────────────────┤
│  /forgot-password                                             │
│  ├── Email input form                                        │
│  ├── Supabase Auth: resetPasswordForEmail()                  │
│  └── Show "Check your email" message                         │
│                                                               │
│  /auth/reset-password (from email link)                      │
│  ├── New password form                                       │
│  ├── Supabase Auth: updateUser({ password })                 │
│  └── Redirect to /login with success                         │
└──────────────────────────────────────────────────────────────┘

4.4 Session Management

Supabase SSR Configuration:

// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  event.locals.supabase = createServerClient(
    PUBLIC_SUPABASE_URL,
    PUBLIC_SUPABASE_ANON_KEY,
    {
      cookies: {
        getAll: () => event.cookies.getAll(),
        setAll: (cookies) => cookies.forEach(({ name, value, options }) =>
          event.cookies.set(name, value, { ...options, path: '/' })
        )
      }
    }
  );

  event.locals.safeGetSession = async () => {
    const { data: { session } } = await event.locals.supabase.auth.getSession();
    if (!session) return { session: null, user: null, member: null };

    const { data: { user } } = await event.locals.supabase.auth.getUser();
    if (!user) return { session: null, user: null, member: null };

    // Fetch member record
    const { data: member } = await event.locals.supabase
      .from('members_with_dues')
      .select('*')
      .eq('id', user.id)
      .single();

    return { session, user, member };
  };

  return resolve(event);
};

4.5 Navigation Structure

Desktop: Collapsible Sidebar

┌─────────────────────────────────────────────────────┐
│ ┌─────┐                                             │
│ │     │  Dashboard                                  │
│ │LOGO │  ─────────────────────────────────────      │
│ │     │                                             │
│ └─────┘  [Sidebar Navigation]        [Content]      │
│                                                     │
│  📊 Dashboard                                       │
│  👤 My Profile                                      │
│  📅 Events                                          │
│  💳 Payments                                        │
│                                                     │
│  ── Board ──────── (if board/admin)                │
│  👥 Members                                         │
│  📋 Dues Management                                 │
│  📅 Event Management                                │
│                                                     │
│  ── Admin ──────── (if admin)                      │
│  ⚙️ Settings                                        │
│  👥 User Management                                 │
│  📄 Documents                                       │
│                                                     │
│  ─────────────────                                  │
│  🚪 Logout                                          │
└─────────────────────────────────────────────────────┘

Mobile: Bottom Navigation Bar

┌─────────────────────────────────────┐
│                                     │
│           [Main Content]            │
│                                     │
│                                     │
├─────────────────────────────────────┤
│  🏠    📅    👤    ⚙️    ☰          │
│ Home  Events Profile Settings More  │
└─────────────────────────────────────┘

4.6 Unified Dashboard with Role Sections

Single /dashboard route with role-based sections:

<!-- routes/(app)/dashboard/+page.svelte -->
<script>
  export let data; // { member, events, stats }
  const { member } = data;
  const isBoard = member.role === 'board' || member.role === 'admin';
  const isAdmin = member.role === 'admin';
</script>

<!-- Everyone sees these -->
<WelcomeCard {member} />
<DuesStatusCard {member} />
<UpcomingEventsCard events={data.upcomingEvents} />

<!-- Board and Admin see these -->
{#if isBoard}
  <Separator title="Board Tools" />
  <MemberStatsCard stats={data.memberStats} />
  <PendingMembersCard members={data.pendingMembers} />
  <DuesOverviewCard overview={data.duesOverview} />
{/if}

<!-- Admin only sees these -->
{#if isAdmin}
  <Separator title="Admin" />
  <SystemHealthCard health={data.systemHealth} />
  <RecentActivityCard activity={data.recentActivity} />
  <QuickActionsCard />
{/if}

4.7 Member Dashboard Section

Components:

  1. Welcome Card - Greeting with name, membership status badge
  2. Dues Status Card - Current status, next due date, quick pay info
  3. Upcoming Events Card - Next 3-5 events with RSVP status
  4. Profile Quick View - Photo, basic info, edit link

Data Loaded:

// routes/(app)/dashboard/+page.server.ts
export const load = async ({ locals }) => {
  const { member } = await locals.safeGetSession();

  const upcomingEvents = await getUpcomingEventsForMember(member.id, 5);

  return {
    member,
    upcomingEvents
  };
};

4.8 Board Dashboard Section

Additional Components (visible to board/admin):

  1. Member Stats Card - Total, active, pending, inactive counts
  2. Pending Members Card - New signups awaiting approval/payment
  3. Dues Overview Card - Current, due soon, overdue breakdown
  4. Recent RSVPs Card - Latest event RSVPs

Board Stats:

interface BoardStats {
  totalMembers: number;
  activeMembers: number;
  pendingMembers: number;
  inactiveMembers: number;
  duesSoon: number;      // Due in next 30 days
  duesOverdue: number;   // Past due date
  upcomingEvents: number;
  pendingRsvps: number;
}

4.9 Admin Dashboard Section

Additional Components (admin only):

  1. System Health Card - Supabase status, email status
  2. Recent Activity Card - Latest logins, signups, payments
  3. Quick Actions Card - Add member, create event, send broadcast
  4. Alerts Card - Issues requiring attention

Admin Stats:

interface AdminStats extends BoardStats {
  totalUsers: number;          // Auth users
  recentLogins: number;        // Last 24 hours
  failedLogins: number;        // Last 24 hours
  emailsSent: number;          // This month
  storageUsed: number;         // MB
}

4.10 Route Protection

Layout-level guards using SvelteKit:

// routes/(app)/+layout.server.ts
import { redirect } from '@sveltejs/kit';

export const load = async ({ locals }) => {
  const { session, member } = await locals.safeGetSession();

  if (!session) {
    throw redirect(303, '/login');
  }

  return { member };
};

// routes/(app)/board/+layout.server.ts
export const load = async ({ locals, parent }) => {
  const { member } = await parent();

  if (member.role !== 'board' && member.role !== 'admin') {
    throw redirect(303, '/dashboard');
  }

  return {};
};

// routes/(app)/admin/+layout.server.ts
export const load = async ({ locals, parent }) => {
  const { member } = await parent();

  if (member.role !== 'admin') {
    throw redirect(303, '/dashboard');
  }

  return {};
};

4.11 Responsive Breakpoints

Breakpoint Width Layout
Mobile < 640px Bottom nav, stacked cards
Tablet 640-1024px Collapsed sidebar rail, 2-column
Desktop > 1024px Full sidebar, 3-column grid

4.12 Dashboard Glass-Morphism Design

Glass Card Base Style:

.glass-card {
  background: rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.glass-card-dark {
  background: rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
}

Monaco Red Accent:

:root {
  --monaco-red: #dc2626;
  --monaco-red-light: #fee2e2;
  --monaco-red-dark: #991b1b;
}

5. DOCUMENT STORAGE (Detailed)

5.1 Document Categories (Admin-Configurable)

CREATE TABLE public.document_categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL,
  description TEXT,
  icon TEXT,                              -- Lucide icon name
  sort_order INTEGER DEFAULT 0,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Default categories
INSERT INTO document_categories (name, display_name, icon) VALUES
  ('meeting_minutes', 'Meeting Minutes', 'file-text'),
  ('governance', 'Governance & Bylaws', 'scale'),
  ('legal', 'Legal Documents', 'briefcase'),
  ('financial', 'Financial Reports', 'dollar-sign'),
  ('member_resources', 'Member Resources', 'book-open'),
  ('forms', 'Forms & Templates', 'clipboard'),
  ('other', 'Other Documents', 'file');

5.2 Upload Permissions

Who can upload:

  • Board members
  • Administrators

Members cannot upload - they can only view documents shared with them.

5.3 Document Visibility (Per-Document)

Visibility Options:

Level Who Can View
public Anyone (no login required)
members All logged-in members
board Board + Admin only
admin Admin only

Custom permissions can also specify specific member IDs for restricted access.

5.4 Document Schema

CREATE TABLE public.documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Basic Info
  title TEXT NOT NULL,
  description TEXT,
  category_id UUID REFERENCES public.document_categories(id),

  -- File Info (Supabase Storage)
  file_path TEXT NOT NULL,               -- Storage path
  file_name TEXT NOT NULL,               -- Original filename
  file_size INTEGER NOT NULL,            -- Bytes
  mime_type TEXT NOT NULL,               -- 'application/pdf', etc.

  -- Visibility
  visibility TEXT NOT NULL DEFAULT 'members'
    CHECK (visibility IN ('public', 'members', 'board', 'admin')),

  -- Optional: Specific member access (for restricted docs)
  allowed_member_ids UUID[],             -- If set, only these members can view

  -- Version tracking
  version INTEGER DEFAULT 1,
  replaces_document_id UUID REFERENCES public.documents(id),

  -- Metadata
  uploaded_by UUID NOT NULL REFERENCES public.members(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Document access log (for audit)
CREATE TABLE public.document_access_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  document_id UUID NOT NULL REFERENCES public.documents(id) ON DELETE CASCADE,
  accessed_by UUID REFERENCES public.members(id),  -- null if public access
  access_type TEXT NOT NULL CHECK (access_type IN ('view', 'download')),
  ip_address TEXT,
  accessed_at TIMESTAMPTZ DEFAULT NOW()
);

5.5 File Storage (Supabase Storage)

Bucket Configuration:

// Storage bucket: 'documents'
// Path structure: documents/{category}/{year}/{filename}

// Example paths:
// documents/meeting_minutes/2026/board-meeting-2026-01-15.pdf
// documents/governance/bylaws-v2.pdf
// documents/financial/2025/annual-report-2025.pdf

Upload Limits:

  • Max file size: 50MB
  • Allowed types: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, JPG, PNG

5.6 Document UI Features

Document Library View:

  • Filter by category
  • Filter by visibility level
  • Search by title/description
  • Sort by date, name, category
  • Grid or list view toggle

Document Card:

┌────────────────────────────────────────┐
│ 📄 [Category Icon]                      │
│                                         │
│ Board Meeting Minutes - January 2026   │
│ Meeting minutes from the monthly...    │
│                                         │
│ 📅 Jan 15, 2026  |  📎 PDF  |  1.2 MB  │
│                                         │
│ [View] [Download]  👁️ Members          │
└────────────────────────────────────────┘

Upload Form (Board/Admin):

  • Title (required)
  • Description (optional)
  • Category (required, dropdown)
  • Visibility (required)
  • Custom access (optional, member multi-select)
  • File upload (drag & drop)

5.7 Document Permissions (RLS)

-- RLS Policies for documents
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;

-- Public documents viewable by anyone
CREATE POLICY "Public documents are viewable"
  ON public.documents FOR SELECT
  USING (visibility = 'public');

-- Member documents viewable by authenticated users
CREATE POLICY "Member documents viewable by members"
  ON public.documents FOR SELECT
  TO authenticated
  USING (
    visibility = 'members'
    OR visibility = 'public'
    OR (visibility = 'board' AND EXISTS (
      SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')
    ))
    OR (visibility = 'admin' AND EXISTS (
      SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'
    ))
    OR (allowed_member_ids IS NOT NULL AND auth.uid() = ANY(allowed_member_ids))
  );

-- Board/Admin can manage documents
CREATE POLICY "Board can upload documents"
  ON public.documents FOR INSERT
  TO authenticated
  WITH CHECK (
    EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
  );

CREATE POLICY "Uploader or admin can update documents"
  ON public.documents FOR UPDATE
  TO authenticated
  USING (
    uploaded_by = auth.uid()
    OR EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
  );

CREATE POLICY "Admin can delete documents"
  ON public.documents FOR DELETE
  TO authenticated
  USING (
    EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
  );

5.8 Version History

Document versioning:

  • When replacing a document, create new record with replaces_document_id
  • Previous versions remain accessible (archived)
  • View version history for any document
-- Get version history for a document
SELECT d.*, m.first_name, m.last_name
FROM public.documents d
JOIN public.members m ON d.uploaded_by = m.id
WHERE d.id = :document_id
   OR d.replaces_document_id = :document_id
   OR d.id IN (
     SELECT replaces_document_id FROM public.documents
     WHERE id = :document_id
   )
ORDER BY d.version DESC;

5.9 Meeting Minutes Special Handling

For meeting minutes category:

  • Date field (meeting date)
  • Attendees list (optional)
  • Agenda reference (optional)
  • Quick template for consistency
-- Optional meeting minutes metadata
ALTER TABLE public.documents ADD COLUMN meeting_date DATE;
ALTER TABLE public.documents ADD COLUMN meeting_attendees UUID[];

6. ADMIN SETTINGS SYSTEM (Detailed)

6.1 Settings Architecture Overview

Centralized configuration for all customizable aspects of the portal, accessible only to Admins via /admin/settings.

Settings Categories:

  1. Organization - Association branding and info
  2. Membership - Statuses, types, and pricing
  3. Dues - Payment settings and reminders
  4. Events - Event types and defaults
  5. Documents - Categories and storage
  6. Directory - Visibility controls
  7. Email - SMTP and template settings
  8. System - Technical settings

6.2 Settings Storage (Unified Table)

-- Flexible key-value settings with JSON support
CREATE TABLE public.app_settings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  category TEXT NOT NULL,                    -- 'organization', 'dues', 'email', etc.
  setting_key TEXT NOT NULL,                 -- 'payment_iban', 'reminder_days', etc.
  setting_value JSONB NOT NULL,              -- Supports strings, numbers, arrays, objects
  setting_type TEXT NOT NULL DEFAULT 'text'  -- 'text', 'number', 'boolean', 'json', 'array'
    CHECK (setting_type IN ('text', 'number', 'boolean', 'json', 'array')),
  display_name TEXT NOT NULL,                -- Human-readable label
  description TEXT,                          -- Help text for admins
  is_public BOOLEAN DEFAULT FALSE,           -- If true, accessible without auth
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  updated_by UUID REFERENCES public.members(id),

  UNIQUE(category, setting_key)
);

-- Audit log for settings changes
CREATE TABLE public.settings_audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  setting_id UUID NOT NULL REFERENCES public.app_settings(id),
  old_value JSONB,
  new_value JSONB NOT NULL,
  changed_by UUID NOT NULL REFERENCES public.members(id),
  changed_at TIMESTAMPTZ DEFAULT NOW(),
  change_reason TEXT
);

-- RLS: Only admins can read/write settings
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Only admins can manage settings"
  ON public.app_settings FOR ALL
  TO authenticated
  USING (
    EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
    OR is_public = TRUE
  );

6.3 Default Settings (Seeded on First Run)

-- Organization Settings
INSERT INTO app_settings (category, setting_key, setting_value, setting_type, display_name, description, is_public) VALUES
  ('organization', 'association_name', '"Monaco USA"', 'text', 'Association Name', 'Official name of the association', true),
  ('organization', 'tagline', '"Americans in Monaco"', 'text', 'Tagline', 'Association tagline shown on login', true),
  ('organization', 'contact_email', '"contact@monacousa.org"', 'text', 'Contact Email', 'Public contact email address', true),
  ('organization', 'address', '"Monaco"', 'text', 'Address', 'Association physical address', true),
  ('organization', 'logo_url', '"/logo.png"', 'text', 'Logo URL', 'Path to association logo', true),
  ('organization', 'primary_color', '"#dc2626"', 'text', 'Primary Color', 'Brand primary color (hex)', true),

-- Dues Settings
  ('dues', 'payment_iban', '"MC58 1756 9000 0104 0050 1001 860"', 'text', 'Payment IBAN', 'Bank IBAN for dues payment', false),
  ('dues', 'payment_account_holder', '"ASSOCIATION MONACO USA"', 'text', 'Account Holder', 'Bank account holder name', false),
  ('dues', 'payment_bank_name', '"Credit Foncier de Monaco"', 'text', 'Bank Name', 'Name of the bank', false),
  ('dues', 'payment_instructions', '"Please include your Member ID (MUSA-XXXX) in the reference"', 'text', 'Payment Instructions', 'Instructions shown to members', false),
  ('dues', 'reminder_days_before', '[30, 7, 1]', 'array', 'Reminder Days', 'Days before due date to send reminders', false),
  ('dues', 'grace_period_days', '30', 'number', 'Grace Period', 'Days after due date before auto-inactive', false),
  ('dues', 'overdue_reminder_interval', '14', 'number', 'Overdue Reminder Interval', 'Days between overdue reminder emails', false),
  ('dues', 'auto_inactive_enabled', 'true', 'boolean', 'Auto Inactive', 'Automatically set members inactive after grace period', false),

-- Event Settings
  ('events', 'default_max_guests', '2', 'number', 'Default Max Guests', 'Default maximum guests per RSVP', false),
  ('events', 'reminder_hours_before', '[24, 1]', 'array', 'Event Reminder Hours', 'Hours before event to send reminders', false),
  ('events', 'allow_public_rsvp', 'true', 'boolean', 'Allow Public RSVP', 'Allow non-members to RSVP to public events', false),
  ('events', 'auto_close_rsvp_hours', '0', 'number', 'Auto Close RSVP', 'Hours before event to close RSVP (0 = never)', false),

-- Directory Settings
  ('directory', 'member_visible_fields', '["first_name", "last_name", "avatar_url", "nationality", "member_since"]', 'array', 'Member Visible Fields', 'Fields visible to regular members', false),
  ('directory', 'board_visible_fields', '["first_name", "last_name", "avatar_url", "nationality", "email", "phone", "address", "date_of_birth", "member_since", "membership_status"]', 'array', 'Board Visible Fields', 'Fields visible to board members', false),
  ('directory', 'show_membership_status', 'false', 'boolean', 'Show Status to Members', 'Show membership status in directory for regular members', false),

-- System Settings
  ('system', 'maintenance_mode', 'false', 'boolean', 'Maintenance Mode', 'Put the portal in maintenance mode', false),
  ('system', 'maintenance_message', '"The portal is currently undergoing maintenance. Please check back soon."', 'text', 'Maintenance Message', 'Message shown during maintenance', false),
  ('system', 'session_timeout_hours', '168', 'number', 'Session Timeout', 'Hours until session expires (default: 7 days)', false),
  ('system', 'max_upload_size_mb', '50', 'number', 'Max Upload Size', 'Maximum file upload size in MB', false),
  ('system', 'allowed_file_types', '["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "jpg", "jpeg", "png", "webp"]', 'array', 'Allowed File Types', 'Allowed file extensions for uploads', false);

6.4 Settings UI Layout

Navigation Tabs:

┌──────────────────────────────────────────────────────────────────┐
│  ⚙️ Settings                                                      │
├──────────────────────────────────────────────────────────────────┤
│  [Organization] [Membership] [Dues] [Events] [Documents]         │
│  [Directory] [Email] [System]                                    │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  Organization Settings                                       │ │
│  │  ─────────────────────────────────────────────────────────  │ │
│  │                                                              │ │
│  │  Association Name                                            │ │
│  │  ┌──────────────────────────────────────────────────────┐   │ │
│  │  │ Monaco USA                                            │   │ │
│  │  └──────────────────────────────────────────────────────┘   │ │
│  │  Official name of the association                            │ │
│  │                                                              │ │
│  │  Tagline                                                     │ │
│  │  ┌──────────────────────────────────────────────────────┐   │ │
│  │  │ Americans in Monaco                                   │   │ │
│  │  └──────────────────────────────────────────────────────┘   │ │
│  │  Association tagline shown on login                          │ │
│  │                                                              │ │
│  │  Primary Color                                               │ │
│  │  ┌────────┐ ┌──────────────────────────────────────────┐    │ │
│  │  │ 🎨     │ │ #dc2626                                   │    │ │
│  │  └────────┘ └──────────────────────────────────────────┘    │ │
│  │                                                              │ │
│  │           [Save Changes]                                     │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘

6.5 Membership Settings Tab

Manages configurable membership statuses and types:

┌──────────────────────────────────────────────────────────────────┐
│  Membership Settings                                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  MEMBERSHIP STATUSES                                             │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  ┌───────────┬─────────────┬──────────┬────────────┬──────────┐ │
│  │ Name      │ Display     │ Color    │ Is Default │ Actions  │ │
│  ├───────────┼─────────────┼──────────┼────────────┼──────────┤ │
│  │ pending   │ Pending     │ 🟡 Yellow│ ✓          │ ✏️ 🗑️     │ │
│  │ active    │ Active      │ 🟢 Green │            │ ✏️ 🗑️     │ │
│  │ inactive  │ Inactive    │ ⚪ Gray  │            │ ✏️ 🗑️     │ │
│  │ expired   │ Expired     │ 🔴 Red   │            │ ✏️ 🗑️     │ │
│  └───────────┴─────────────┴──────────┴────────────┴──────────┘ │
│                                                                  │
│  [+ Add Status]                                                  │
│                                                                  │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  MEMBERSHIP TYPES                                                │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  ┌───────────┬───────────────┬──────────┬────────────┬────────┐ │
│  │ Name      │ Display       │ Annual € │ Is Default │Actions │ │
│  ├───────────┼───────────────┼──────────┼────────────┼────────┤ │
│  │ regular   │ Regular       │ €50.00   │ ✓          │ ✏️ 🗑️   │ │
│  │ student   │ Student       │ €25.00   │            │ ✏️ 🗑️   │ │
│  │ senior    │ Senior (65+)  │ €35.00   │            │ ✏️ 🗑️   │ │
│  │ family    │ Family        │ €75.00   │            │ ✏️ 🗑️   │ │
│  │ honorary  │ Honorary      │ €0.00    │            │ ✏️ 🗑️   │ │
│  └───────────┴───────────────┴──────────┴────────────┴────────┘ │
│                                                                  │
│  [+ Add Membership Type]                                         │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

6.6 Event Types Settings

Admin can manage event types with colors and icons:

┌──────────────────────────────────────────────────────────────────┐
│  Event Types                                                     │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┬───────────────┬────────────┬────────┬────────┐ │
│  │ Name        │ Display       │ Color      │ Icon   │Actions │ │
│  ├─────────────┼───────────────┼────────────┼────────┼────────┤ │
│  │ social      │ Social Event  │ 🟢 #10b981 │ 🎉     │ ✏️ 🗑️   │ │
│  │ meeting     │ Meeting       │ 🔵 #6366f1 │ 👥     │ ✏️ 🗑️   │ │
│  │ fundraiser  │ Fundraiser    │ 🟠 #f59e0b │ 💝     │ ✏️ 🗑️   │ │
│  │ workshop    │ Workshop      │ 🟣 #8b5cf6 │ 🎓     │ ✏️ 🗑️   │ │
│  │ gala        │ Gala/Formal   │ 🌸 #ec4899 │ ✨     │ ✏️ 🗑️   │ │
│  │ other       │ Other         │ ⚫ #6b7280 │ 📅     │ ✏️ 🗑️   │ │
│  └─────────────┴───────────────┴────────────┴────────┴────────┘ │
│                                                                  │
│  [+ Add Event Type]                                              │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

6.7 Document Categories Settings

┌──────────────────────────────────────────────────────────────────┐
│  Document Categories                                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────┬─────────────────────┬────────┬────────────┐ │
│  │ Name            │ Display             │ Icon   │ Actions    │ │
│  ├─────────────────┼─────────────────────┼────────┼────────────┤ │
│  │ meeting_minutes │ Meeting Minutes     │ 📄     │ ✏️ 🗑️       │ │
│  │ governance      │ Governance & Bylaws │ ⚖️     │ ✏️ 🗑️       │ │
│  │ legal           │ Legal Documents     │ 💼     │ ✏️ 🗑️       │ │
│  │ financial       │ Financial Reports   │ 💰     │ ✏️ 🗑️       │ │
│  │ member_resources│ Member Resources    │ 📚     │ ✏️ 🗑️       │ │
│  │ forms           │ Forms & Templates   │ 📋     │ ✏️ 🗑️       │ │
│  │ other           │ Other Documents     │ 📁     │ ✏️ 🗑️       │ │
│  └─────────────────┴─────────────────────┴────────┴────────────┘ │
│                                                                  │
│  [+ Add Category]                                                │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

6.8 Directory Visibility Settings

Admin controls what fields are visible to different roles:

┌──────────────────────────────────────────────────────────────────┐
│  Directory Visibility                                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Configure which member fields are visible in the directory.     │
│  Admin always sees all fields.                                   │
│                                                                  │
│  ┌─────────────────┬──────────────────┬──────────────────┐      │
│  │ Field           │ Visible to       │ Visible to       │      │
│  │                 │ Members          │ Board            │      │
│  ├─────────────────┼──────────────────┼──────────────────┤      │
│  │ First Name      │ ☑️ Always shown   │ ☑️ Always shown   │      │
│  │ Last Name       │ ☑️ Always shown   │ ☑️ Always shown   │      │
│  │ Profile Photo   │ ☑️                │ ☑️                │      │
│  │ Nationality     │ ☑️                │ ☑️                │      │
│  │ Email           │ ☐                │ ☑️                │      │
│  │ Phone           │ ☐                │ ☑️                │      │
│  │ Address         │ ☐                │ ☑️                │      │
│  │ Date of Birth   │ ☐                │ ☑️                │      │
│  │ Member Since    │ ☑️                │ ☑️                │      │
│  │ Status          │ ☐                │ ☑️                │      │
│  │ Membership Type │ ☐                │ ☑️                │      │
│  └─────────────────┴──────────────────┴──────────────────┘      │
│                                                                  │
│  [Save Visibility Settings]                                      │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

6.9 System Settings Tab

┌──────────────────────────────────────────────────────────────────┐
│  System Settings                                                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  MAINTENANCE                                                     │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  ☐ Enable Maintenance Mode                                       │
│                                                                  │
│  Maintenance Message:                                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ The portal is currently undergoing maintenance.          │   │
│  │ Please check back soon.                                  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  SECURITY                                                        │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  Session Timeout (hours):                                        │
│  ┌────────────┐                                                  │
│  │ 168        │ (7 days)                                        │
│  └────────────┘                                                  │
│                                                                  │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  FILE UPLOADS                                                    │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  Max Upload Size (MB):                                           │
│  ┌────────────┐                                                  │
│  │ 50         │                                                  │
│  └────────────┘                                                  │
│                                                                  │
│  Allowed File Types:                                             │
│  [PDF] [DOC] [DOCX] [XLS] [XLSX] [PPT] [PPTX]                   │
│  [TXT] [JPG] [PNG] [WEBP] [+ Add Type]                          │
│                                                                  │
│  [Save System Settings]                                          │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

6.10 Settings Access Pattern

// src/lib/server/settings.ts

// Get a single setting with type safety
export async function getSetting<T>(
  supabase: SupabaseClient,
  category: string,
  key: string,
  defaultValue: T
): Promise<T> {
  const { data } = await supabase
    .from('app_settings')
    .select('setting_value')
    .eq('category', category)
    .eq('setting_key', key)
    .single();

  return data?.setting_value ?? defaultValue;
}

// Get all settings for a category
export async function getCategorySettings(
  supabase: SupabaseClient,
  category: string
): Promise<Record<string, any>> {
  const { data } = await supabase
    .from('app_settings')
    .select('setting_key, setting_value')
    .eq('category', category);

  return Object.fromEntries(
    (data ?? []).map(s => [s.setting_key, s.setting_value])
  );
}

// Update a setting (admin only)
export async function updateSetting(
  supabase: SupabaseClient,
  category: string,
  key: string,
  value: any,
  userId: string
): Promise<void> {
  await supabase
    .from('app_settings')
    .update({
      setting_value: value,
      updated_at: new Date().toISOString(),
      updated_by: userId
    })
    .eq('category', category)
    .eq('setting_key', key);
}

6.11 Settings Permissions

Action Member Board Admin
View public settings
View all settings - -
Edit settings - -
Manage statuses - -
Manage membership types - -
Manage event types - -
Manage document categories - -
View settings audit log - -

7. EMAIL SYSTEM (Detailed)

7.1 Email Architecture

Provider: Supabase Edge Functions + external SMTP (Resend, SendGrid, or Mailgun)

Why external SMTP:

  • Supabase built-in email is limited to auth emails only
  • External SMTP provides better deliverability, tracking, and templates
  • Resend recommended for simplicity and modern API

7.2 Email Provider Configuration

-- Email settings (stored in app_settings)
INSERT INTO app_settings (category, setting_key, setting_value, setting_type, display_name, description) VALUES
  ('email', 'provider', '"resend"', 'text', 'Email Provider', 'Email service provider (resend, sendgrid, mailgun)'),
  ('email', 'api_key', '""', 'text', 'API Key', 'Email provider API key (stored securely)'),
  ('email', 'from_address', '"noreply@monacousa.org"', 'text', 'From Address', 'Default sender email address'),
  ('email', 'from_name', '"Monaco USA"', 'text', 'From Name', 'Default sender name'),
  ('email', 'reply_to', '"contact@monacousa.org"', 'text', 'Reply-To Address', 'Reply-to email address'),
  ('email', 'enable_tracking', 'true', 'boolean', 'Enable Tracking', 'Track email opens and clicks'),
  ('email', 'batch_size', '50', 'number', 'Batch Size', 'Max emails per batch send'),
  ('email', 'rate_limit_per_hour', '100', 'number', 'Rate Limit', 'Maximum emails per hour');

7.3 Email Types & Triggers

Email Type Trigger Recipients Automated
welcome New signup verified New member Yes
email_verification Signup New member Yes (Supabase)
password_reset Password reset request Member Yes (Supabase)
dues_reminder X days before due Member Yes (cron)
dues_due_today Due date Member Yes (cron)
dues_overdue Every X days overdue Member Yes (cron)
dues_lapsed Grace period ends Member Yes (cron)
dues_received Payment recorded Member Yes
event_created New event published All/visibility Optional
event_reminder X hours before event RSVP'd members Yes (cron)
event_updated Event details changed RSVP'd members Yes
event_cancelled Event cancelled RSVP'd members Yes
rsvp_confirmation RSVP submitted Member Yes
waitlist_promoted Spot opens up Waitlisted member Yes
member_invite Admin invites member Invitee Manual
broadcast Admin sends message Selected members Manual

7.4 Email Templates Schema

CREATE TABLE public.email_templates (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Template identification
  template_key TEXT UNIQUE NOT NULL,         -- 'dues_reminder', 'welcome', etc.
  template_name TEXT NOT NULL,               -- 'Dues Reminder Email'
  category TEXT NOT NULL,                    -- 'dues', 'events', 'system'

  -- Template content
  subject TEXT NOT NULL,                     -- Subject line with {{variables}}
  body_html TEXT NOT NULL,                   -- HTML body with {{variables}}
  body_text TEXT,                            -- Plain text fallback

  -- Settings
  is_active BOOLEAN DEFAULT TRUE,
  is_system BOOLEAN DEFAULT FALSE,           -- System templates can't be deleted

  -- Metadata
  variables_schema JSONB,                    -- Available variables documentation
  preview_data JSONB,                        -- Sample data for preview

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  updated_by UUID REFERENCES public.members(id)
);

-- Default email templates
INSERT INTO email_templates (template_key, template_name, category, subject, body_html, is_system, variables_schema) VALUES

-- Welcome Email
('welcome', 'Welcome Email', 'system',
 'Welcome to Monaco USA, {{member_name}}!',
 '<!DOCTYPE html>
<html>
<head><style>body{font-family:Arial,sans-serif;}</style></head>
<body>
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <img src="{{logo_url}}" alt="Monaco USA" style="max-width:150px;">
  <h1>Welcome to Monaco USA!</h1>
  <p>Dear {{member_name}},</p>
  <p>Thank you for joining Monaco USA! Your Member ID is <strong>{{member_id}}</strong>.</p>
  <p>To complete your membership, please pay your annual dues of <strong>€{{dues_amount}}</strong>.</p>
  <h3>Payment Details:</h3>
  <ul>
    <li>Bank: {{bank_name}}</li>
    <li>IBAN: {{iban}}</li>
    <li>Account Holder: {{account_holder}}</li>
    <li>Reference: {{member_id}}</li>
  </ul>
  <p><a href="{{portal_link}}" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">Access Your Portal</a></p>
  <p>Best regards,<br>Monaco USA Team</p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","member_id":"string","dues_amount":"number","bank_name":"string","iban":"string","account_holder":"string","portal_link":"string","logo_url":"string"}'
),

-- Dues Reminder
('dues_reminder', 'Dues Reminder', 'dues',
 'Your Monaco USA dues are due in {{days_until_due}} days',
 '<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <h1>Dues Reminder</h1>
  <p>Dear {{member_name}},</p>
  <p>This is a friendly reminder that your Monaco USA membership dues of <strong>€{{dues_amount}}</strong> are due on <strong>{{due_date}}</strong> ({{days_until_due}} days from now).</p>
  <h3>Payment Details:</h3>
  <ul>
    <li>IBAN: {{iban}}</li>
    <li>Account Holder: {{account_holder}}</li>
    <li>Reference: {{member_id}}</li>
  </ul>
  <p><a href="{{portal_link}}/payments" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Payment Details</a></p>
  <p>Thank you for being a valued member!</p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","member_id":"string","dues_amount":"number","due_date":"date","days_until_due":"number","iban":"string","account_holder":"string","portal_link":"string"}'
),

-- Dues Overdue
('dues_overdue', 'Dues Overdue Notice', 'dues',
 'OVERDUE: Your Monaco USA dues are {{days_overdue}} days past due',
 '<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <h1 style="color:#dc2626;">Payment Overdue</h1>
  <p>Dear {{member_name}},</p>
  <p>Your Monaco USA membership dues of <strong>€{{dues_amount}}</strong> are now <strong>{{days_overdue}} days overdue</strong>.</p>
  <p>Please make your payment as soon as possible to maintain your membership benefits.</p>
  {{#if grace_period_remaining}}
  <p><strong>Note:</strong> You have {{grace_period_remaining}} days remaining in your grace period before your membership is set to inactive.</p>
  {{/if}}
  <h3>Payment Details:</h3>
  <ul>
    <li>IBAN: {{iban}}</li>
    <li>Account Holder: {{account_holder}}</li>
    <li>Reference: {{member_id}}</li>
  </ul>
  <p><a href="{{portal_link}}/payments" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">Pay Now</a></p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","member_id":"string","dues_amount":"number","days_overdue":"number","grace_period_remaining":"number","iban":"string","account_holder":"string","portal_link":"string"}'
),

-- Dues Received
('dues_received', 'Payment Confirmation', 'dues',
 'Thank you! Your Monaco USA dues payment has been received',
 '<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <h1 style="color:#10b981;">Payment Received!</h1>
  <p>Dear {{member_name}},</p>
  <p>Thank you! We have received your membership dues payment.</p>
  <h3>Payment Details:</h3>
  <ul>
    <li>Amount: €{{amount_paid}}</li>
    <li>Payment Date: {{payment_date}}</li>
    <li>Next Due Date: {{next_due_date}}</li>
    <li>Reference: {{payment_reference}}</li>
  </ul>
  <p>Your membership is now active until {{next_due_date}}.</p>
  <p><a href="{{portal_link}}/payments" style="background:#10b981;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Payment History</a></p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","amount_paid":"number","payment_date":"date","next_due_date":"date","payment_reference":"string","portal_link":"string"}'
),

-- Event RSVP Confirmation
('rsvp_confirmation', 'RSVP Confirmation', 'events',
 'You''re registered: {{event_title}}',
 '<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <h1>You''re Registered!</h1>
  <p>Dear {{member_name}},</p>
  <p>Your RSVP for <strong>{{event_title}}</strong> has been confirmed.</p>
  <h3>Event Details:</h3>
  <ul>
    <li><strong>Date:</strong> {{event_date}}</li>
    <li><strong>Time:</strong> {{event_time}}</li>
    <li><strong>Location:</strong> {{event_location}}</li>
    {{#if guest_count}}<li><strong>Additional Guests:</strong> {{guest_count}}</li>{{/if}}
  </ul>
  {{#if is_paid}}
  <h3>Payment Required:</h3>
  <p>Total: €{{total_amount}}</p>
  <ul>
    <li>IBAN: {{iban}}</li>
    <li>Reference: {{payment_reference}}</li>
  </ul>
  {{/if}}
  <p><a href="{{portal_link}}/events/{{event_id}}" style="background:#dc2626;color:white;padding:10px 20px;text-decoration:none;border-radius:5px;">View Event</a></p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","event_title":"string","event_date":"date","event_time":"string","event_location":"string","guest_count":"number","is_paid":"boolean","total_amount":"number","iban":"string","payment_reference":"string","event_id":"string","portal_link":"string"}'
),

-- Event Reminder
('event_reminder', 'Event Reminder', 'events',
 'Reminder: {{event_title}} is {{time_until_event}}',
 '<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;">
  <h1>Event Reminder</h1>
  <p>Dear {{member_name}},</p>
  <p>This is a reminder that <strong>{{event_title}}</strong> is {{time_until_event}}.</p>
  <h3>Event Details:</h3>
  <ul>
    <li><strong>Date:</strong> {{event_date}}</li>
    <li><strong>Time:</strong> {{event_time}}</li>
    <li><strong>Location:</strong> {{event_location}}</li>
  </ul>
  {{#if event_description}}
  <p>{{event_description}}</p>
  {{/if}}
  <p>We look forward to seeing you there!</p>
</div>
</body>
</html>',
 TRUE,
 '{"member_name":"string","event_title":"string","event_date":"date","event_time":"string","event_location":"string","event_description":"string","time_until_event":"string"}'
);

7.5 Email Logging Schema

-- Enhanced email logs with tracking
CREATE TABLE public.email_logs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Recipients
  recipient_id UUID REFERENCES public.members(id),
  recipient_email TEXT NOT NULL,
  recipient_name TEXT,

  -- Email details
  template_key TEXT REFERENCES public.email_templates(template_key),
  subject TEXT NOT NULL,
  email_type TEXT NOT NULL,

  -- Status tracking
  status TEXT NOT NULL DEFAULT 'queued'
    CHECK (status IN ('queued', 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')),

  -- Provider data
  provider TEXT,                             -- 'resend', 'sendgrid', etc.
  provider_message_id TEXT,                  -- External message ID for tracking

  -- Engagement tracking
  opened_at TIMESTAMPTZ,
  clicked_at TIMESTAMPTZ,

  -- Error handling
  error_message TEXT,
  retry_count INTEGER DEFAULT 0,

  -- Metadata
  template_variables JSONB,                  -- Variables used in template
  sent_by UUID REFERENCES public.members(id), -- For manual sends

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  sent_at TIMESTAMPTZ,
  delivered_at TIMESTAMPTZ
);

-- Index for common queries
CREATE INDEX idx_email_logs_recipient ON public.email_logs(recipient_id);
CREATE INDEX idx_email_logs_status ON public.email_logs(status);
CREATE INDEX idx_email_logs_type ON public.email_logs(email_type);
CREATE INDEX idx_email_logs_created ON public.email_logs(created_at DESC);

7.6 Automated Email Scheduler (Supabase Edge Function)

// supabase/functions/email-scheduler/index.ts

import { createClient } from '@supabase/supabase-js';
import { Resend } from 'resend';

// Runs daily via pg_cron
Deno.serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const today = new Date();
  today.setHours(0, 0, 0, 0);

  // 1. Get settings
  const settings = await getSettings(supabase);
  const reminderDays = settings.reminder_days_before as number[];
  const gracePeriod = settings.grace_period_days as number;

  // 2. Find members needing reminders
  const { data: membersWithDues } = await supabase
    .from('members_with_dues')
    .select('*')
    .in('dues_status', ['current', 'due_soon', 'overdue']);

  for (const member of membersWithDues || []) {
    const dueDate = new Date(member.current_due_date);
    const daysUntil = Math.ceil((dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
    const daysOverdue = member.days_overdue || 0;

    // Check if we need to send a reminder
    if (daysUntil > 0 && reminderDays.includes(daysUntil)) {
      // Send upcoming reminder
      await sendEmail(supabase, 'dues_reminder', member, {
        days_until_due: daysUntil
      });
    } else if (daysUntil === 0) {
      // Due today
      await sendEmail(supabase, 'dues_due_today', member, {});
    } else if (daysOverdue > 0 && daysOverdue <= gracePeriod) {
      // Overdue but in grace period
      if (daysOverdue % settings.overdue_reminder_interval === 0) {
        await sendEmail(supabase, 'dues_overdue', member, {
          days_overdue: daysOverdue,
          grace_period_remaining: gracePeriod - daysOverdue
        });
      }
    } else if (daysOverdue === gracePeriod + 1) {
      // Grace period just ended
      await sendEmail(supabase, 'dues_lapsed', member, {});
    }
  }

  // 3. Send event reminders
  await sendEventReminders(supabase, settings);

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' }
  });
});

async function sendEmail(
  supabase: any,
  templateKey: string,
  member: any,
  extraVariables: Record<string, any>
) {
  // Get template
  const { data: template } = await supabase
    .from('email_templates')
    .select('*')
    .eq('template_key', templateKey)
    .eq('is_active', true)
    .single();

  if (!template) return;

  // Build variables
  const variables = {
    member_name: `${member.first_name} ${member.last_name}`,
    member_id: member.member_id,
    dues_amount: member.annual_dues || 50,
    due_date: member.current_due_date,
    portal_link: Deno.env.get('PORTAL_URL'),
    ...extraVariables
  };

  // Add payment settings
  const settings = await getSettings(supabase);
  variables.iban = settings.payment_iban;
  variables.account_holder = settings.payment_account_holder;
  variables.bank_name = settings.payment_bank_name;

  // Render template
  const subject = renderTemplate(template.subject, variables);
  const html = renderTemplate(template.body_html, variables);

  // Send via provider
  const resend = new Resend(Deno.env.get('RESEND_API_KEY'));
  const result = await resend.emails.send({
    from: `${settings.from_name} <${settings.from_address}>`,
    to: member.email,
    subject,
    html
  });

  // Log email
  await supabase.from('email_logs').insert({
    recipient_id: member.id,
    recipient_email: member.email,
    recipient_name: variables.member_name,
    template_key: templateKey,
    subject,
    email_type: templateKey,
    status: result.error ? 'failed' : 'sent',
    provider: 'resend',
    provider_message_id: result.data?.id,
    error_message: result.error?.message,
    template_variables: variables,
    sent_at: new Date().toISOString()
  });
}

7.7 Email Settings UI (Admin)

┌──────────────────────────────────────────────────────────────────┐
│  Email Settings                                                  │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  PROVIDER CONFIGURATION                                          │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  Email Provider:                                                 │
│  ┌────────────────────────────────────────┐                     │
│  │ Resend                              ▼  │                     │
│  └────────────────────────────────────────┘                     │
│                                                                  │
│  API Key:                                                        │
│  ┌────────────────────────────────────────┐                     │
│  │ re_•••••••••••••••                     │ [Test Connection]   │
│  └────────────────────────────────────────┘                     │
│                                                                  │
│  From Address:                                                   │
│  ┌────────────────────────────────────────┐                     │
│  │ noreply@monacousa.org                  │                     │
│  └────────────────────────────────────────┘                     │
│                                                                  │
│  From Name:                                                      │
│  ┌────────────────────────────────────────┐                     │
│  │ Monaco USA                             │                     │
│  └────────────────────────────────────────┘                     │
│                                                                  │
│  Reply-To:                                                       │
│  ┌────────────────────────────────────────┐                     │
│  │ contact@monacousa.org                  │                     │
│  └────────────────────────────────────────┘                     │
│                                                                  │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  TRACKING                                                        │
│  ─────────────────────────────────────────────────────────────  │
│                                                                  │
│  ☑️ Enable open tracking                                         │
│  ☑️ Enable click tracking                                        │
│                                                                  │
│  [Save Email Settings]                                           │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.8 Email Templates Editor (Admin)

┌──────────────────────────────────────────────────────────────────┐
│  Email Templates                                                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌───────────────────┬────────────────┬─────────┬──────────────┐ │
│  │ Template          │ Category       │ Active  │ Actions      │ │
│  ├───────────────────┼────────────────┼─────────┼──────────────┤ │
│  │ Welcome Email     │ System         │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Dues Reminder     │ Dues           │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Dues Due Today    │ Dues           │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Dues Overdue      │ Dues           │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Dues Lapsed       │ Dues           │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Dues Received     │ Dues           │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ RSVP Confirmation │ Events         │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Event Reminder    │ Events         │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Event Cancelled   │ Events         │ ✓       │ ✏️ 👁️ 📧      │ │
│  │ Waitlist Promoted │ Events         │ ✓       │ ✏️ 👁️ 📧      │ │
│  └───────────────────┴────────────────┴─────────┴──────────────┘ │
│                                                                  │
│  Legend: ✏️ Edit  |  👁️ Preview  |  📧 Send Test                │
│                                                                  │
│  [+ Create Custom Template]                                      │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.9 Template Editor View

┌──────────────────────────────────────────────────────────────────┐
│  Edit Template: Dues Reminder                                    │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Template Name:                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ Dues Reminder                                             │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Subject Line:                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ Your Monaco USA dues are due in {{days_until_due}} days   │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────┬───────────────────────────────────┐   │
│  │ HTML Editor          │ Preview                           │   │
│  ├──────────────────────┼───────────────────────────────────┤   │
│  │ <h1>Dues Reminder</h1│                                   │   │
│  │ <p>Dear {{member_nam │  ┌─────────────────────────────┐  │   │
│  │ e}},</p>             │  │     Dues Reminder           │  │   │
│  │ <p>This is a friendl │  │                             │  │   │
│  │ y reminder that your │  │  Dear John Doe,             │  │   │
│  │ Monaco USA membershi │  │                             │  │   │
│  │ p dues...</p>        │  │  This is a friendly         │  │   │
│  │ ...                  │  │  reminder that your Monaco  │  │   │
│  │                      │  │  USA membership dues of     │  │   │
│  │                      │  │  €50.00 are due on...       │  │   │
│  │                      │  └─────────────────────────────┘  │   │
│  └──────────────────────┴───────────────────────────────────┘   │
│                                                                  │
│  AVAILABLE VARIABLES:                                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ {{member_name}} {{member_id}} {{dues_amount}} {{due_date}}│   │
│  │ {{days_until_due}} {{iban}} {{account_holder}}            │   │
│  │ {{portal_link}}                                           │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  [Cancel] [Send Test Email] [Save Changes]                       │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.10 Email Logs View (Admin)

┌──────────────────────────────────────────────────────────────────┐
│  Email Logs                                                      │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Filter: [All Types ▼] [All Status ▼] [Last 30 days ▼] [Search] │
│                                                                  │
│  ┌────────┬────────────────┬──────────────┬────────┬──────────┐ │
│  │ Date   │ Recipient      │ Subject      │ Type   │ Status   │ │
│  ├────────┼────────────────┼──────────────┼────────┼──────────┤ │
│  │ Jan 9  │ john@email.com │ Your Monaco  │ dues_  │ 📬 Opened │ │
│  │ 14:30  │ John Doe       │ USA dues...  │ remind │          │ │
│  ├────────┼────────────────┼──────────────┼────────┼──────────┤ │
│  │ Jan 9  │ jane@email.com │ You're regis │ rsvp_  │ ✅ Sent   │ │
│  │ 10:15  │ Jane Smith     │ tered: Gala  │ conf   │          │ │
│  ├────────┼────────────────┼──────────────┼────────┼──────────┤ │
│  │ Jan 8  │ bob@email.com  │ OVERDUE: You │ dues_  │ 🔴 Bounce │ │
│  │ 09:00  │ Bob Wilson     │ r Monaco...  │ overdu │          │ │
│  └────────┴────────────────┴──────────────┴────────┴──────────┘ │
│                                                                  │
│  Status Legend:                                                  │
│  ✅ Sent | 📬 Opened | 🔗 Clicked | 🔴 Bounced | ❌ Failed      │
│                                                                  │
│  Stats: 156 sent this month | 78% open rate | 2 bounces         │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.11 Manual Broadcast Feature (Admin)

Admin can send broadcast emails to selected members:

┌──────────────────────────────────────────────────────────────────┐
│  Send Broadcast Email                                            │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Recipients:                                                     │
│  ○ All active members (45)                                       │
│  ○ All members (52)                                              │
│  ○ Board members only (5)                                        │
│  ● Select specific members                                       │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ 🔍 Search members...                                      │   │
│  ├──────────────────────────────────────────────────────────┤   │
│  │ ☑️ John Doe (john@email.com)                              │   │
│  │ ☑️ Jane Smith (jane@email.com)                            │   │
│  │ ☐ Bob Wilson (bob@email.com)                              │   │
│  │ ...                                                       │   │
│  └──────────────────────────────────────────────────────────┘   │
│  Selected: 2 members                                             │
│                                                                  │
│  Subject:                                                        │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ Important Update from Monaco USA                          │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Message:                                                        │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ [Rich text editor with formatting options]                │   │
│  │                                                           │   │
│  │ Dear {{member_name}},                                     │   │
│  │                                                           │   │
│  │ We wanted to inform you about...                          │   │
│  │                                                           │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  [Preview] [Send Test to Myself] [Send to 2 Recipients]          │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

7.12 Email Cron Jobs (pg_cron in Supabase)

-- Enable pg_cron extension
CREATE EXTENSION IF NOT EXISTS pg_cron;

-- Schedule daily email checks (runs at 9 AM Monaco time)
SELECT cron.schedule(
  'daily-email-scheduler',
  '0 9 * * *',  -- Every day at 9:00 AM
  $$
  SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/email-scheduler',
    headers := '{"Authorization": "Bearer ' || current_setting('app.service_role_key') || '"}'::jsonb,
    body := '{}'::jsonb
  );
  $$
);

-- Schedule event reminders (runs every hour)
SELECT cron.schedule(
  'hourly-event-reminders',
  '0 * * * *',  -- Every hour
  $$
  SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/event-reminders',
    headers := '{"Authorization": "Bearer ' || current_setting('app.service_role_key') || '"}'::jsonb,
    body := '{}'::jsonb
  );
  $$
);

7.13 Email Permissions

Action Member Board Admin
Receive automated emails
View own email history
View all email logs - -
Edit email templates - -
Send broadcast emails - -
Send manual reminders -
Configure email settings - -
Test email connection - -

Phase 1: Current System Analysis (COMPLETED)

Current Tech Stack

Layer Technology
Framework Nuxt 3 (Vue 3) - SSR disabled, CSR-only
UI Components Vuetify 3 + Tailwind CSS + Custom SCSS
Database NocoDB (REST API over PostgreSQL)
Authentication Keycloak (OAuth2/OIDC)
File Storage MinIO (S3-compatible)
Email Nodemailer + Handlebars templates
State Vue Composition API (no Pinia)

Current Features Inventory

1. Member Management

  • Data Model: Member with 20+ fields (name, email, phone, DOB, nationality, address, etc.)
  • Member ID Format: MUSA-YYYY-XXXX (auto-generated)
  • Status States: Active, Inactive, Pending, Expired
  • Portal Tiers: admin, board, user
  • Profile Images: Stored in MinIO

2. Dues/Subscription System

  • Calculation: Due 1 year after last payment
  • Fields Tracked: membership_date_paid, payment_due_date, current_year_dues_paid
  • Reminders: 30-day advance warning, overdue notifications
  • Auto-Status: Members 1+ year overdue marked Inactive
  • Rates: €50 regular, €25 student, €35 senior, €75 family, €200 corporate

3. Events System

  • Event Types: meeting, social, fundraiser, workshop, board-only
  • RSVP System: confirmed, declined, pending, waitlist
  • Guest Management: Extra guests per RSVP
  • Pricing: Member vs non-member pricing
  • Visibility: public, board-only, admin-only
  • Calendar: FullCalendar integration with iCal feed

4. Authentication & Authorization

  • Login Methods: OAuth (Keycloak) + Direct login (ROPC)
  • Role System: Keycloak realm roles (monaco-admin, monaco-board, monaco-user)
  • Session: Server-side with HTTP-only cookies (7-30 days)
  • Rate Limiting: 5 attempts/15min, 1-hour IP block
  • Signup Flow: Form → reCAPTCHA → Keycloak user → NocoDB member → Verification email

5. Three Dashboard Types

  • Admin: Full system control, user management, settings, all features
  • Board: Member directory, dues management, events, meetings, governance
  • Member: Personal profile, events, payments, resources

Current Pain Points (to address in rebuild)

  1. CSR-only limits SEO and initial load performance
  2. NocoDB adds complexity vs direct database access
  3. String booleans ("true"/"false") cause type issues
  4. No payment history table (only last payment tracked)
  5. Vuetify + Tailwind overlap creates CSS conflicts
  6. Large monolithic layout files (700-800+ lines each)

Phase 2: New System Requirements

Core Requirements (confirmed)

  • Beautiful, modern, responsive frontend (not generic Vue look)
  • Member tracking with subscription/dues management
  • Dues due 1 year after last payment
  • Event calendar with RSVP system
  • Board members can create/manage events
  • Event features: capacity, +1 guests, member/non-member pricing
  • Three dashboard types: Admin, Board, Member
  • Signup system similar to current
  • Manual payment tracking only (no Stripe integration)
  • Email notifications (dues reminders, event updates)
  • Document storage (meeting minutes, governance docs)

Deployment Strategy

  • Replace entirely - Switch over when ready (no parallel systems)
  • Manual data entry - Members will be entered manually (no migration scripts)

FRONTEND FRAMEWORK OPTIONS

1. Qwik + QwikCity MOST INNOVATIVE

Aspect Details
What it is Resumable framework - HTML loads instantly, JS loads on-demand
Unique Feature Zero hydration - fastest possible load times
Learning Curve Medium (JSX-like but different mental model)
Ecosystem Growing - Auth.js, Drizzle, Modular Forms integrations
Benchmark Score 93.8 (highest among all frameworks)

Pros:

  • Blazing fast initial load (no hydration delay)
  • Feels like React/JSX but with better performance model
  • Built-in form handling with Zod validation
  • Server functions with "use server" directive
  • Excellent TypeScript support
  • Unique - won't look like every other site

Cons:

  • Smaller community than React/Vue
  • Fewer pre-built component libraries
  • Newer framework (less battle-tested in production)
  • Some patterns feel unfamiliar at first

Best For: Performance-focused apps where first impression matters


2. SolidStart (Solid.js) MOST PERFORMANT REACTIVITY

Aspect Details
What it is Fine-grained reactive framework with meta-framework
Unique Feature No Virtual DOM - direct DOM updates via signals
Learning Curve Medium (React-like JSX, different reactivity)
Ecosystem Good - Ark UI, Kobalte for components
Benchmark Score 92.2

Pros:

  • Smallest bundle sizes in the industry
  • React-like syntax (easy transition)
  • True reactivity (no re-renders, just updates)
  • Server functions and data loading built-in
  • Growing rapidly in popularity
  • Unique performance characteristics

Cons:

  • Smaller ecosystem than React
  • Fewer tutorials and resources
  • Some React patterns don't translate directly
  • Component libraries less mature

Best For: Highly interactive dashboards with lots of real-time updates


3. SvelteKit BEST DEVELOPER EXPERIENCE

Aspect Details
What it is Compiler-based framework with full-stack capabilities
Unique Feature No virtual DOM, compiles to vanilla JS
Learning Curve Low (closest to vanilla HTML/CSS/JS)
Ecosystem Strong - Skeleton UI, Melt UI, Shadcn-Svelte
Benchmark Score 91.0

Pros:

  • Simplest syntax - looks like enhanced HTML
  • Smallest learning curve
  • Excellent built-in animations/transitions
  • Strong TypeScript integration
  • Great form handling
  • Active, helpful community
  • Svelte 5 runes make state even simpler

Cons:

  • Different mental model from React/Vue
  • Smaller job market (if that matters)
  • Some advanced patterns less documented
  • Breaking changes between Svelte 4 and 5

Best For: Clean, maintainable code with minimal boilerplate


4. Astro + React/Vue/Svelte Islands MOST FLEXIBLE

Aspect Details
What it is Content-focused framework with "islands" of interactivity
Unique Feature Mix multiple frameworks, zero JS by default
Learning Curve Low-Medium
Ecosystem Excellent - use ANY UI library
Benchmark Score 90.2

Pros:

  • Use React, Vue, Svelte, or Solid components together
  • Zero JavaScript shipped by default
  • Excellent for content + interactive sections
  • Built-in image optimization
  • Great Supabase integration documented
  • View Transitions API support

Cons:

  • Not ideal for highly interactive SPAs
  • Island architecture adds complexity
  • More configuration for full interactivity
  • Less unified than single-framework approach

Best For: Marketing site + member portal hybrid


Tier 2: Battle-Tested Mainstream

5. Next.js 15 (React)

Aspect Details
What it is Most popular React meta-framework
Unique Feature App Router, Server Components, huge ecosystem
Learning Curve Medium-High (lots of concepts)
Ecosystem Largest - shadcn/ui, Radix, everything
Benchmark Score N/A (didn't query)

Pros:

  • Largest ecosystem and community
  • Most job opportunities
  • shadcn/ui provides beautiful, customizable components
  • Excellent documentation
  • Vercel hosting optimized

Cons:

  • Can feel "generic" - many sites use it
  • Complex mental model (Server vs Client components)
  • Heavier than alternatives
  • Vercel-centric development

6. Remix

Aspect Details
What it is Full-stack React framework focused on web standards
Unique Feature Nested routing, progressive enhancement
Learning Curve Medium
Ecosystem Good - React ecosystem compatible
Benchmark Score 89.4

Pros:

  • Web standards focused (works without JS)
  • Excellent data loading patterns
  • Great error handling
  • Form handling is first-class
  • Can deploy anywhere (not Vercel-locked)

Cons:

  • Smaller community than Next.js
  • Less "magic" means more manual work
  • Merged with React Router (transition period)

7. TanStack Start (React)

Aspect Details
What it is New full-stack framework from TanStack team
Unique Feature Type-safe from database to UI
Learning Curve Medium
Ecosystem TanStack Query, Form, Router built-in
Benchmark Score 80.7

Pros:

  • Built by TanStack (Query, Router, Form authors)
  • End-to-end type safety
  • Modern patterns throughout
  • Excellent data fetching built-in

Cons:

  • Very new (beta/early stage)
  • Smaller community
  • Less documentation
  • Rapidly evolving API

8. Nuxt 4 (Vue 3)

Aspect Details
What it is Latest Vue meta-framework
Unique Feature Familiar from current system
Learning Curve Low (you know it)
Ecosystem Good - Nuxt UI, PrimeVue

Pros:

  • Familiar - no learning curve
  • Can reuse some current code/patterns
  • Strong conventions
  • Good TypeScript support now

Cons:

  • User specifically wants to avoid "generic Vue look"
  • Similar limitations to current system
  • Less innovative than alternatives

9. Angular 19

Aspect Details
What it is Google's enterprise framework
Unique Feature Signals, standalone components, full framework
Learning Curve High
Ecosystem Enterprise-grade
Benchmark Score 90.3

Pros:

  • Complete framework (no decisions to make)
  • Excellent for large applications
  • Strong typing throughout
  • Signals in Angular 19 are modern

Cons:

  • Steeper learning curve
  • More verbose
  • "Enterprise" feel may not fit small org
  • Overkill for this scale

Tier 3: Experimental/Niche

10. Leptos (Rust)

Aspect Details
What it is Full-stack Rust framework
Unique Feature WASM-based, extremely fast
Benchmark Score 89.7

Pros:

  • Blazing fast (Rust + WASM)
  • Type safety at compile time
  • Innovative approach

Cons:

  • Requires learning Rust
  • Small ecosystem
  • Harder to find developers
  • Overkill for this use case

11. Hono + HTMX

Aspect Details
What it is Lightweight backend + hypermedia frontend
Unique Feature Server-rendered, minimal JS
Benchmark Score 92.8

Pros:

  • Extremely lightweight
  • Simple mental model
  • Works on edge (Cloudflare Workers)
  • Fast development

Cons:

  • Less rich interactivity
  • Different paradigm (hypermedia)
  • Limited complex UI patterns
  • Manual work for dashboards

UI COMPONENT LIBRARY OPTIONS

For React-based Frameworks (Next.js, Remix, TanStack)

Library Style Customizable Notes
shadcn/ui Modern, clean Fully (copy/paste) Most popular, highly customizable
Radix Themes Polished Theme-based Beautiful defaults, less work
Radix Primitives Unstyled Fully Build completely custom
Ark UI Unstyled Fully Works with multiple frameworks
Park UI Pre-styled Ark Moderate Ark + beautiful defaults

For Solid.js

Library Style Notes
Kobalte Unstyled Radix-like primitives for Solid
Ark UI Solid Unstyled Same Ark, Solid version
Solid UI Various Community components

For Svelte

Library Style Notes
shadcn-svelte Modern Port of shadcn for Svelte
Skeleton UI Tailwind Full design system
Melt UI Unstyled Primitives for Svelte
Bits UI Unstyled Headless components

For Qwik

Library Style Notes
Qwik UI Official Growing component library
Custom + Tailwind Any Build from scratch

For Vue/Nuxt

Library Style Notes
shadcn-vue Modern Port of shadcn for Vue
Radix Vue Unstyled Radix primitives for Vue
Nuxt UI Tailwind Official Nuxt components
PrimeVue Various Comprehensive but generic

DATABASE OPTIONS - DETAILED COMPARISON

Aspect Details
Type PostgreSQL + Auth + Storage + Realtime
Hosting Managed cloud or self-hosted
Pricing Free tier, then $25/mo

Pros:

  • All-in-one: Database + Auth + File Storage + Realtime
  • PostgreSQL (industry standard, powerful)
  • Row-level security built-in
  • Excellent TypeScript support
  • Auto-generated APIs
  • Real-time subscriptions
  • Built-in auth (replaces Keycloak)
  • Dashboard for data management
  • Can self-host if needed

Cons:

  • Vendor lock-in (mitigated by self-host option)
  • Learning curve for RLS policies
  • Free tier has limits
  • Less control than raw PostgreSQL

Best For: Rapid development with full-stack features


Option 2: PostgreSQL + Prisma

Aspect Details
Type Direct database + Type-safe ORM
Hosting Any PostgreSQL host (Neon, Railway, etc.)
Pricing Database hosting costs only

Pros:

  • Full control over database
  • Prisma schema is very readable
  • Excellent TypeScript types
  • Migrations handled automatically
  • Works with any PostgreSQL
  • Large community

Cons:

  • Need separate auth solution
  • Need separate file storage
  • More setup work
  • Prisma can be slow for complex queries

Best For: Maximum control and flexibility


Option 3: PostgreSQL + Drizzle ORM

Aspect Details
Type Direct database + Lightweight ORM
Hosting Any PostgreSQL host
Pricing Database hosting costs only

Pros:

  • Closer to SQL (less abstraction)
  • Faster than Prisma
  • Smaller bundle size
  • TypeScript-first
  • Better for complex queries
  • Growing rapidly

Cons:

  • Newer, smaller community
  • Less documentation
  • Need separate auth/storage
  • More manual migration work

Best For: Performance-critical apps, SQL-comfortable teams


Option 4: PlanetScale + Drizzle

Aspect Details
Type Serverless MySQL
Hosting Managed cloud only
Pricing Free tier, then usage-based

Pros:

  • Serverless scaling
  • Branching (like git for databases)
  • No connection limits
  • Fast globally

Cons:

  • MySQL not PostgreSQL
  • No foreign keys (by design)
  • Vendor lock-in
  • Can get expensive at scale

Best For: Serverless deployments, edge functions


Option 5: Keep NocoDB

Aspect Details
Type Spreadsheet-like interface over database
Hosting Self-hosted or cloud

Pros:

  • Already configured
  • Non-technical users can edit data
  • Flexible schema changes
  • API already exists

Cons:

  • Adds complexity layer
  • String booleans issue
  • Less type safety
  • Performance overhead
  • Limited query capabilities

Best For: Non-technical admin users need direct access


AUTHENTICATION OPTIONS - DETAILED COMPARISON

Option 1: Supabase Auth IF USING SUPABASE

Aspect Details
Type Built into Supabase
Providers Email, OAuth (Google, GitHub, etc.), Magic Link

Pros:

  • Integrated with Supabase (one platform)
  • Row-level security integration
  • Simple setup
  • Built-in user management
  • Social logins included
  • Magic link support

Cons:

  • Tied to Supabase
  • Less customizable than Keycloak
  • No SAML/enterprise SSO on free tier

Option 2: Keep Keycloak

Aspect Details
Type Self-hosted identity provider
Providers Everything (OIDC, SAML, social, etc.)

Pros:

  • Already configured and working
  • Enterprise-grade features
  • Full control
  • SAML support
  • Custom themes
  • User federation

Cons:

  • Complex to maintain
  • Heavy resource usage
  • Overkill for small org
  • Requires Java expertise
  • Self-hosted burden

Option 3: Better Auth MODERN CHOICE

Aspect Details
Type Framework-agnostic TypeScript auth
Providers Email, OAuth, Magic Link, Passkeys

Pros:

  • Modern, TypeScript-first
  • Works with any framework
  • Plugin system for features
  • Session management built-in
  • Two-factor auth support
  • Lightweight

Cons:

  • Newer (less battle-tested)
  • Self-implemented
  • Need own user storage

Option 4: Auth.js (NextAuth)

Aspect Details
Type Framework-agnostic auth library
Providers 50+ OAuth providers

Pros:

  • Massive provider support
  • Well documented
  • Active development
  • Works with Qwik, SvelteKit, etc.

Cons:

  • Complex configuration
  • Database adapter setup
  • v5 migration issues
  • Can be finicky

Option 5: Clerk

Aspect Details
Type Auth-as-a-service
Providers Everything + beautiful UI

Pros:

  • Beautiful pre-built components
  • Zero config setup
  • Great DX
  • Organizations/teams built-in

Cons:

  • Expensive at scale
  • Vendor lock-in
  • Less control
  • Monthly costs

Option 6: Lucia Auth

Aspect Details
Type Low-level auth library
Note Being deprecated in favor of guides

Pros:

  • Full control
  • Lightweight
  • Educational

Cons:

  • Being sunset
  • More DIY work

CHOSEN STACK (FINAL)

Layer Technology Rationale
Framework SvelteKit 2 Best DX, simple syntax, excellent performance
UI Components shadcn-svelte + Bits UI Beautiful, customizable, accessible
Styling Tailwind CSS 4 Utility-first, works great with shadcn
Database Supabase (PostgreSQL) All-in-one, managed, real-time capable
Auth Supabase Auth Integrated with database, simple setup
File Storage Supabase Storage Profile images, documents
Design Glass-morphism (evolved) Modern, distinctive, refined
Language TypeScript Type safety throughout

Phase 3: Architecture Design

Project Structure

monacousa-portal-2026/
├── src/
│   ├── lib/
│   │   ├── components/          # Reusable UI components
│   │   │   ├── ui/              # shadcn-svelte base components
│   │   │   ├── dashboard/       # Dashboard widgets
│   │   │   ├── members/         # Member-related components
│   │   │   ├── events/          # Event components
│   │   │   └── layout/          # Layout components (sidebar, header)
│   │   ├── server/              # Server-only utilities
│   │   │   ├── supabase.ts      # Supabase server client
│   │   │   └── auth.ts          # Auth helpers
│   │   ├── stores/              # Svelte stores for state
│   │   ├── utils/               # Shared utilities
│   │   │   ├── types.ts         # TypeScript types
│   │   │   ├── constants.ts     # App constants
│   │   │   └── helpers.ts       # Helper functions
│   │   └── supabase.ts          # Supabase client (browser)
│   │
│   ├── routes/
│   │   ├── +layout.svelte       # Root layout
│   │   ├── +layout.server.ts    # Root server load (auth)
│   │   ├── +page.svelte         # Landing page
│   │   │
│   │   ├── (auth)/              # Auth group (guest only)
│   │   │   ├── login/
│   │   │   ├── signup/
│   │   │   ├── forgot-password/
│   │   │   └── callback/        # OAuth callback
│   │   │
│   │   ├── (app)/               # Protected app group
│   │   │   ├── +layout.svelte   # App layout with sidebar
│   │   │   ├── +layout.server.ts # Auth guard
│   │   │   │
│   │   │   ├── dashboard/       # User dashboard
│   │   │   ├── profile/         # User profile
│   │   │   ├── events/          # Events calendar/list
│   │   │   ├── payments/        # Dues/payments view
│   │   │   │
│   │   │   ├── board/           # Board-only routes
│   │   │   │   ├── +layout.server.ts  # Board guard
│   │   │   │   ├── dashboard/
│   │   │   │   ├── members/
│   │   │   │   ├── events/      # Event management
│   │   │   │   └── meetings/
│   │   │   │
│   │   │   └── admin/           # Admin-only routes
│   │   │       ├── +layout.server.ts  # Admin guard
│   │   │       ├── dashboard/
│   │   │       ├── members/
│   │   │       ├── users/
│   │   │       ├── events/
│   │   │       └── settings/
│   │   │
│   │   └── api/                 # API routes (if needed)
│   │
│   ├── hooks.server.ts          # Server hooks (Supabase SSR)
│   └── app.d.ts                 # TypeScript declarations
│
├── static/                      # Static assets
├── supabase/                    # Supabase local dev
│   └── migrations/              # Database migrations
├── tests/                       # Test files
├── svelte.config.js
├── tailwind.config.ts
├── vite.config.ts
└── package.json

Database Schema (Supabase/PostgreSQL)

-- USERS (managed by Supabase Auth)
-- auth.users table is automatic

-- MEMBERS (extends auth.users)
CREATE TABLE public.members (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  member_id TEXT UNIQUE NOT NULL,          -- MUSA-2026-0001 format
  first_name TEXT NOT NULL,
  last_name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  phone TEXT,
  date_of_birth DATE,
  address TEXT,
  nationality TEXT[],                       -- Array of country codes ['FR', 'US']

  -- Membership
  role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'board', 'admin')),
  membership_status TEXT NOT NULL DEFAULT 'pending'
    CHECK (membership_status IN ('active', 'inactive', 'pending', 'expired')),
  member_since DATE DEFAULT CURRENT_DATE,

  -- Profile
  avatar_url TEXT,
  bio TEXT,

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- DUES/PAYMENTS (tracks payment history)
CREATE TABLE public.dues_payments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,

  amount DECIMAL(10,2) NOT NULL,
  currency TEXT DEFAULT 'EUR',
  payment_date DATE NOT NULL,
  due_date DATE NOT NULL,                   -- When this payment period ends
  payment_method TEXT,                      -- 'bank_transfer', 'cash', etc.
  reference TEXT,                           -- Transaction reference
  notes TEXT,

  recorded_by UUID REFERENCES public.members(id), -- Who recorded this payment
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- EVENTS
CREATE TABLE public.events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  title TEXT NOT NULL,
  description TEXT,
  event_type TEXT NOT NULL CHECK (event_type IN ('social', 'meeting', 'fundraiser', 'workshop', 'other')),

  start_datetime TIMESTAMPTZ NOT NULL,
  end_datetime TIMESTAMPTZ NOT NULL,
  location TEXT,

  -- Capacity & Pricing
  max_attendees INTEGER,
  max_guests_per_member INTEGER DEFAULT 1,
  member_price DECIMAL(10,2) DEFAULT 0,
  non_member_price DECIMAL(10,2) DEFAULT 0,

  -- Visibility
  visibility TEXT NOT NULL DEFAULT 'members'
    CHECK (visibility IN ('public', 'members', 'board', 'admin')),
  status TEXT NOT NULL DEFAULT 'published'
    CHECK (status IN ('draft', 'published', 'cancelled', 'completed')),

  -- Metadata
  created_by UUID NOT NULL REFERENCES public.members(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- EVENT RSVPs
CREATE TABLE public.event_rsvps (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
  member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,

  status TEXT NOT NULL DEFAULT 'confirmed'
    CHECK (status IN ('confirmed', 'declined', 'waitlist', 'cancelled')),
  guest_count INTEGER DEFAULT 0,
  guest_names TEXT[],

  payment_status TEXT DEFAULT 'not_required'
    CHECK (payment_status IN ('not_required', 'pending', 'paid')),

  attended BOOLEAN DEFAULT FALSE,

  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(event_id, member_id)
);

-- DOCUMENTS (meeting minutes, governance, etc.)
CREATE TABLE public.documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  title TEXT NOT NULL,
  description TEXT,
  category TEXT NOT NULL CHECK (category IN ('meeting_minutes', 'governance', 'financial', 'other')),

  file_path TEXT NOT NULL,           -- Supabase Storage path
  file_name TEXT NOT NULL,
  file_size INTEGER,
  mime_type TEXT,

  visibility TEXT NOT NULL DEFAULT 'board'
    CHECK (visibility IN ('members', 'board', 'admin')),

  uploaded_by UUID NOT NULL REFERENCES public.members(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- EMAIL NOTIFICATIONS LOG
CREATE TABLE public.email_logs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  recipient_id UUID REFERENCES public.members(id),
  recipient_email TEXT NOT NULL,
  email_type TEXT NOT NULL,          -- 'dues_reminder', 'event_invite', etc.
  subject TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'sent'
    CHECK (status IN ('sent', 'failed', 'bounced')),

  sent_at TIMESTAMPTZ DEFAULT NOW(),
  error_message TEXT
);

-- COMPUTED VIEW: Member with dues status
CREATE VIEW public.members_with_dues AS
SELECT
  m.*,
  dp.payment_date as last_payment_date,
  dp.due_date as current_due_date,
  CASE
    WHEN dp.due_date IS NULL THEN 'never_paid'
    WHEN dp.due_date < CURRENT_DATE THEN 'overdue'
    WHEN dp.due_date < CURRENT_DATE + INTERVAL '30 days' THEN 'due_soon'
    ELSE 'current'
  END as dues_status,
  CASE
    WHEN dp.due_date < CURRENT_DATE
    THEN CURRENT_DATE - dp.due_date
    ELSE NULL
  END as days_overdue
FROM public.members m
LEFT JOIN LATERAL (
  SELECT payment_date, due_date
  FROM public.dues_payments
  WHERE member_id = m.id
  ORDER BY due_date DESC
  LIMIT 1
) dp ON true;

-- ROW LEVEL SECURITY
ALTER TABLE public.members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.dues_payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.events ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.event_rsvps ENABLE ROW LEVEL SECURITY;

-- Members: Users can read all, update own, admins can do anything
CREATE POLICY "Members are viewable by authenticated users"
  ON public.members FOR SELECT
  TO authenticated
  USING (true);

CREATE POLICY "Users can update own profile"
  ON public.members FOR UPDATE
  TO authenticated
  USING (auth.uid() = id);

CREATE POLICY "Admins can insert members"
  ON public.members FOR INSERT
  TO authenticated
  WITH CHECK (
    EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
    OR auth.uid() = id  -- Self-registration
  );

-- Events: Based on visibility
CREATE POLICY "Events viewable based on visibility"
  ON public.events FOR SELECT
  TO authenticated
  USING (
    visibility = 'members'
    OR visibility = 'public'
    OR (visibility = 'board' AND EXISTS (
      SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')
    ))
    OR (visibility = 'admin' AND EXISTS (
      SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'
    ))
  );

-- Board/Admin can manage events
CREATE POLICY "Board can manage events"
  ON public.events FOR ALL
  TO authenticated
  USING (
    EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
  );

Authentication Flow

1. SIGNUP FLOW
   ┌─────────────┐     ┌──────────────┐     ┌─────────────┐
   │  /signup    │────▶│ Supabase Auth│────▶│ Email Verify│
   │  Form       │     │ signUp()     │     │ Link Sent   │
   └─────────────┘     └──────────────┘     └─────────────┘
                              │
                              ▼
                       ┌──────────────┐
                       │ Create Member│
                       │ Record (RLS) │
                       └──────────────┘

2. LOGIN FLOW
   ┌─────────────┐     ┌──────────────┐     ┌─────────────┐
   │  /login     │────▶│ Supabase Auth│────▶│ Set Session │
   │  Form       │     │ signIn()     │     │ Cookie      │
   └─────────────┘     └──────────────┘     └─────────────┘
                              │
                              ▼
                       ┌──────────────┐
                       │ Redirect to  │
                       │ Dashboard    │
                       └──────────────┘

3. PROTECTED ROUTES
   ┌─────────────┐     ┌──────────────┐     ┌─────────────┐
   │ Request     │────▶│ hooks.server │────▶│ Check Role  │
   │ /admin/*    │     │ getSession() │     │ in members  │
   └─────────────┘     └──────────────┘     └─────────────┘
                              │                    │
                              ▼                    ▼
                       ┌──────────────┐     ┌─────────────┐
                       │ Valid?       │     │ Redirect or │
                       │ Yes → Render │     │ 403 Error   │
                       └──────────────┘     └─────────────┘

UI Component Library

Using shadcn-svelte with custom glass-morphism theme:

// tailwind.config.ts - Glass theme extensions
export default {
  theme: {
    extend: {
      colors: {
        monaco: {
          50: '#fef2f2',
          100: '#fee2e2',
          500: '#ef4444',
          600: '#dc2626',  // Primary
          700: '#b91c1c',
          900: '#7f1d1d',
        }
      },
      backdropBlur: {
        xs: '2px',
      },
      boxShadow: {
        'glass': '0 8px 32px rgba(0, 0, 0, 0.1)',
        'glass-lg': '0 25px 50px rgba(0, 0, 0, 0.15)',
      }
    }
  }
}

Custom Glass Components:

  • GlassCard - Frosted glass container
  • GlassSidebar - Navigation sidebar
  • GlassButton - Glass-effect buttons
  • GlassInput - Form inputs with glass styling
  • StatCard - Dashboard stat display
  • EventCard - Event display card
  • MemberCard - Member profile card
  • DuesStatusBadge - Dues status indicator

Key Features Implementation

1. Member Management

  • View all members (admin/board)
  • Edit member details
  • Upload profile photos (Supabase Storage)
  • Track membership status
  • Filter by status, nationality, dues

2. Dues Tracking

  • Payment history table
  • Auto-calculate due dates (1 year from payment)
  • Visual status indicators
  • Overdue notifications
  • Manual payment recording

3. Event System

  • Calendar view (FullCalendar or custom)
  • List view with filters
  • RSVP with guest management
  • Attendance tracking
  • Event creation (board/admin)

4. Three Dashboards

Dashboard Features
Member Profile, upcoming events, dues status, quick actions
Board Member stats, pending applications, dues overview, event management
Admin System stats, user management, all member data, settings

5. Email Notifications

  • Dues reminder emails (30 days before, on due date, overdue)
  • Event invitation/updates
  • Welcome email on signup
  • Password reset emails (Supabase built-in)

6. Document Storage

  • Upload meeting minutes, governance docs
  • Organize by category
  • Visibility controls (members/board/admin)
  • Download/preview functionality

Phase 4: Implementation Roadmap

Stage 1: Foundation (Week 1-2)

  1. Initialize SvelteKit project with TypeScript
  2. Set up Tailwind CSS 4 + shadcn-svelte
  3. Configure Supabase project
  4. Create database schema + migrations
  5. Implement Supabase SSR hooks
  6. Build base layout components

Stage 2: Authentication (Week 2-3)

  1. Login/Signup pages
  2. Email verification flow
  3. Password reset
  4. Protected route guards
  5. Role-based access control

Stage 3: Core Features (Week 3-5)

  1. Member dashboard
  2. Profile management
  3. Member directory (board/admin)
  4. Dues tracking system
  5. Payment recording

Stage 4: Events (Week 5-6)

  1. Event listing/calendar
  2. Event detail view
  3. RSVP system
  4. Event creation (board)
  5. Attendance tracking

Stage 5: Admin Features (Week 6-7)

  1. Admin dashboard
  2. User management
  3. System settings
  4. Data export

Stage 6: Polish (Week 7-8)

  1. Glass-morphism styling refinement
  2. Responsive design
  3. Performance optimization
  4. Testing
  5. Documentation

Verification Plan

Development Testing

# Start Supabase local
npx supabase start

# Run dev server
npm run dev

# Type checking
npm run check

Manual Testing Checklist

  • User can sign up and receive verification email
  • User can log in and see dashboard
  • Member can view/edit profile
  • Member can view events and RSVP
  • Board member can access board dashboard
  • Board member can create/manage events
  • Board member can view member directory
  • Board member can record dues payments
  • Admin can access all features
  • Admin can manage user roles
  • Role-based routing works correctly
  • Responsive on mobile/tablet/desktop

Browser Testing

  • Chrome, Firefox, Safari, Edge
  • iOS Safari, Android Chrome

Data Entry Strategy

Since members will be entered manually (no automated migration):

Admin Setup

  1. Create first admin account via Supabase dashboard
  2. Manually set role = 'admin' in members table
  3. Admin can then add other members through the portal

Member Entry Options

  1. Admin adds members - Admin creates accounts for existing members
  2. Self-registration - Members sign up themselves
  3. Invite system - Admin sends email invites with signup links

Initial Launch Checklist

  • Admin account created and verified
  • Board member accounts created
  • Test member account for verification
  • Email templates configured (Supabase)

Files to Create

Path Purpose
monacousa-portal-2026/ New project root
src/hooks.server.ts Supabase SSR setup
src/lib/supabase.ts Client initialization
src/lib/server/supabase.ts Server client
src/routes/+layout.svelte Root layout
src/routes/(auth)/login/+page.svelte Login page
src/routes/(auth)/signup/+page.svelte Signup page
src/routes/(app)/+layout.svelte App layout
src/routes/(app)/dashboard/+page.svelte Member dashboard
supabase/migrations/001_schema.sql Database schema