monacousa-portal/ARCHITECTURE.md

3711 lines
147 KiB
Markdown

# 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:**
```sql
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:**
```sql
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:
```sql
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
```sql
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)
```sql
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:**
```typescript
// 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
```sql
-- 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)
```sql
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:**
```sql
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:**
```sql
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:**
```typescript
// 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
```sql
-- 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):**
```typescript
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
```sql
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:**
```typescript
// 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:**
```svelte
<!-- 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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```css
.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:**
```css
:root {
--monaco-red: #dc2626;
--monaco-red-light: #fee2e2;
--monaco-red-dark: #991b1b;
}
```
---
## 5. DOCUMENT STORAGE (Detailed)
### 5.1 Document Categories (Admin-Configurable)
```sql
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
```sql
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:**
```typescript
// 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)
```sql
-- 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
```sql
-- 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
```sql
-- 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)
```sql
-- 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)
```sql
-- 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
```typescript
// 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
```sql
-- 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
```sql
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
```sql
-- 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)
```typescript
// 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)
```sql
-- 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)
- [x] Beautiful, modern, responsive frontend (not generic Vue look)
- [x] Member tracking with subscription/dues management
- [x] Dues due 1 year after last payment
- [x] Event calendar with RSVP system
- [x] Board members can create/manage events
- [x] Event features: capacity, +1 guests, member/non-member pricing
- [x] Three dashboard types: Admin, Board, Member
- [x] Signup system similar to current
- [x] **Manual payment tracking only** (no Stripe integration)
- [x] **Email notifications** (dues reminders, event updates)
- [x] **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
### Tier 1: Modern & Distinctive (Recommended)
#### 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
### Option 1: **Supabase** ⭐ RECOMMENDED
| 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)
```sql
-- 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:
```typescript
// 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
```bash
# 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 |