+
+
+
+
+
-
-
Preview:
+
+ Preview:
{tagName || "Tag name"}
-
-
-
diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts
new file mode 100644
index 0000000..64bfd69
--- /dev/null
+++ b/src/routes/admin/+page.server.ts
@@ -0,0 +1,104 @@
+import { error, redirect } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+
+// Cast helper for columns not yet in generated types
+function db(supabase: any) {
+ return supabase as any;
+}
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const { session, user } = await locals.safeGetSession();
+
+ if (!session || !user) {
+ redirect(303, '/login');
+ }
+
+ // Check platform admin status
+ const { data: profile } = await db(locals.supabase)
+ .from('profiles')
+ .select('is_platform_admin')
+ .eq('id', user.id)
+ .single();
+
+ if (!profile?.is_platform_admin) {
+ error(403, 'Access denied. Platform admin only.');
+ }
+
+ // Fetch all platform data in parallel
+ const [
+ orgsResult,
+ profilesResult,
+ eventsResult,
+ orgMembersResult,
+ ] = await Promise.all([
+ db(locals.supabase)
+ .from('organizations')
+ .select('*')
+ .order('created_at', { ascending: false }),
+ db(locals.supabase)
+ .from('profiles')
+ .select('id, email, full_name, avatar_url, is_platform_admin, created_at')
+ .order('created_at', { ascending: false }),
+ db(locals.supabase)
+ .from('events')
+ .select('id, name, slug, status, start_date, end_date, org_id, created_at')
+ .order('created_at', { ascending: false }),
+ db(locals.supabase)
+ .from('org_members')
+ .select('id, user_id, org_id, role')
+ .order('created_at', { ascending: false }),
+ ]);
+
+ const organizations = orgsResult.data ?? [];
+ const profiles = profilesResult.data ?? [];
+ const events = eventsResult.data ?? [];
+ const orgMembers = orgMembersResult.data ?? [];
+
+ // Compute stats
+ const now = new Date();
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+
+ const newUsersLast30d = profiles.filter(
+ (p: any) => p.created_at && new Date(p.created_at) > thirtyDaysAgo
+ ).length;
+ const newUsersLast7d = profiles.filter(
+ (p: any) => p.created_at && new Date(p.created_at) > sevenDaysAgo
+ ).length;
+ const activeEvents = events.filter((e: any) => e.status === 'active').length;
+ const planningEvents = events.filter((e: any) => e.status === 'planning').length;
+
+ // Org member counts
+ const orgMemberCounts: Record
= {};
+ for (const m of orgMembers) {
+ orgMemberCounts[m.org_id] = (orgMemberCounts[m.org_id] || 0) + 1;
+ }
+
+ // Org event counts
+ const orgEventCounts: Record = {};
+ for (const e of events) {
+ if (e.org_id) {
+ orgEventCounts[e.org_id] = (orgEventCounts[e.org_id] || 0) + 1;
+ }
+ }
+
+ return {
+ organizations: organizations.map((o: any) => ({
+ ...o,
+ memberCount: orgMemberCounts[o.id] || 0,
+ eventCount: orgEventCounts[o.id] || 0,
+ })),
+ profiles,
+ events,
+ stats: {
+ totalUsers: profiles.length,
+ totalOrgs: organizations.length,
+ totalEvents: events.length,
+ totalMemberships: orgMembers.length,
+ newUsersLast30d,
+ newUsersLast7d,
+ activeEvents,
+ planningEvents,
+ },
+ };
+};
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte
new file mode 100644
index 0000000..c4af90b
--- /dev/null
+++ b/src/routes/admin/+page.svelte
@@ -0,0 +1,406 @@
+
+
+
+ Platform Admin | Root
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(activeTab = v)}
+ />
+
+
+ {#if activeTab === "overview"}
+
+
+
+
+
Recent Organizations
+ (activeTab = "organizations")}
+ >
+ View all →
+
+
+
+ {#each data.organizations.slice(0, 5) as org}
+
+
+
+
+
{org.name}
+
/{org.slug}
+
+
+
+ {org.memberCount} members
+ {org.eventCount} events
+
+
+ {/each}
+ {#if data.organizations.length === 0}
+
No organizations yet
+ {/if}
+
+
+
+
+
+
+
Recent Users
+ (activeTab = "users")}
+ >
+ View all →
+
+
+
+ {#each data.profiles.slice(0, 5) as profile}
+
+
+
+
+
{profile.full_name ?? "No name"}
+
{profile.email}
+
+
+
+ {#if profile.is_platform_admin}
+ Admin
+ {/if}
+ {timeAgo(profile.created_at)}
+
+
+ {/each}
+ {#if data.profiles.length === 0}
+
No users yet
+ {/if}
+
+
+
+
+
+
+
Recent Events
+ (activeTab = "events")}
+ >
+ View all →
+
+
+ {#if data.events.length > 0}
+
+
+
+
+ | Event |
+ Organization |
+ Status |
+ Dates |
+ Created |
+
+
+
+ {#each data.events.slice(0, 8) as event}
+
+ |
+ {event.name}
+ /{event.slug}
+ |
+
+ {orgMap[event.org_id]?.name ?? "—"}
+ |
+
+
+ {event.status}
+
+ |
+
+ {formatDate(event.start_date)} — {formatDate(event.end_date)}
+ |
+ {timeAgo(event.created_at)} |
+
+ {/each}
+
+
+
+ {:else}
+
No events yet
+ {/if}
+
+
+
+ {:else if activeTab === "organizations"}
+
+
+
+
+
+
+
+
+ | Organization |
+ Slug |
+ Members |
+ Events |
+ Created |
+
+
+
+ {#each filteredOrgs as org}
+
+ |
+
+ |
+ /{org.slug} |
+
+ {org.memberCount}
+ |
+
+ {org.eventCount}
+ |
+ {formatDate(org.created_at)} |
+
+ {/each}
+ {#if filteredOrgs.length === 0}
+
+ |
+ {orgSearch ? "No organizations match your search" : "No organizations yet"}
+ |
+
+ {/if}
+
+
+
+
{filteredOrgs.length} of {data.organizations.length} organizations
+
+
+ {:else if activeTab === "users"}
+
+
+
+
+
+
+
+
+ | User |
+ Email |
+ Role |
+ Joined |
+
+
+
+ {#each filteredUsers as profile}
+
+
+
+
+ {profile.full_name ?? "No name"}
+
+ |
+ {profile.email} |
+
+ {#if profile.is_platform_admin}
+ Platform Admin
+ {:else}
+ User
+ {/if}
+ |
+ {formatDate(profile.created_at)} |
+
+ {/each}
+ {#if filteredUsers.length === 0}
+
+ |
+ {userSearch ? "No users match your search" : "No users yet"}
+ |
+
+ {/if}
+
+
+
+
{filteredUsers.length} of {data.profiles.length} users
+
+
+ {:else if activeTab === "events"}
+
+
+
+
+
+
+
+
+ | Event |
+ Organization |
+ Status |
+ Start |
+ End |
+ Created |
+
+
+
+ {#each filteredEvents as event}
+
+
+
+ {event.name}
+ /{event.slug}
+
+ |
+
+ {orgMap[event.org_id]?.name ?? "—"}
+ |
+
+
+ {event.status}
+
+ |
+ {formatDate(event.start_date)} |
+ {formatDate(event.end_date)} |
+ {timeAgo(event.created_at)} |
+
+ {/each}
+ {#if filteredEvents.length === 0}
+
+ |
+ {eventSearch ? "No events match your search" : "No events yet"}
+ |
+
+ {/if}
+
+
+
+
{filteredEvents.length} of {data.events.length} events
+
+ {/if}
+
+
diff --git a/src/routes/invite/[token]/+page.svelte b/src/routes/invite/[token]/+page.svelte
index a1e2863..c4eb26a 100644
--- a/src/routes/invite/[token]/+page.svelte
+++ b/src/routes/invite/[token]/+page.svelte
@@ -1,6 +1,6 @@
-
-
-
+
+
+
{#if data.error}
-
+ error
-
+
Invalid Invite
-
{data.error}
+
{data.error}
goto("/")}>Go Home
{:else if data.invite}
-
+
You're Invited!
-
You've been invited to join
-
+
You've been invited to join
+
{data.invite.org.name}
-
as {data.invite.role}
+
as {data.invite.role}
{#if error}
+ error
{error}
{/if}
{#if data.user}
-
- Signed in as
+ Signed in as {data.user.email}
-
+
Accept Invite & Join
-
+
Wrong account? Sign out
{:else}
-
+
Sign in or create an account to accept this invite.
- Sign In
- Sign In
+ Create Account
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
index 62fb7b3..3d51e6b 100644
--- a/src/routes/login/+page.svelte
+++ b/src/routes/login/+page.svelte
@@ -1,5 +1,5 @@
Style Guide | Root
-
-
-
-
-
-
- arrow_back
-
-
Style Guide
-
All UI components and their variants
-
+
+
+
-
+
+
+
-
+
Colors
-
-
-
-
Background
-
#05090F
+
+ {#each colors as c}
+
+ {/each}
+
+
+
+
+
+ Typography
+
+
+
Headings — Tilt Warp
+
+ {#each typographyScale as t}
+
+ {t.label}
+ {t.text}
+
+ {/each}
+
-
-
-
-
-
-
-
-
-
Error
-
#E03D00
+
+
Body — Work Sans
+
+ {#each bodyScale as b}
+
+ {/each}
+
-
-
- Buttons
-
-
+
+
-
-
- Inputs
-
-
-
-
-
-
-
-
-
-
+
+
-
+
Textarea
-
-
-
-
+
+
+
-
+
Select
-
-
-
-
+
+
+
-
-
- Avatars
-
-
+
+
+ Avatar
+
Sizes
-
-
-
-
+ {#each ["xs", "sm", "md", "lg", "xl"] as size}
+
+ {/each}
-
-
-
Color Generation
-
-
-
-
-
-
+
+ {#each ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace"] as name}
+
+ {/each}
-
-
- Chips
-
-
+
+
+ Badge
+
Variants
- Primary
- Success
- Warning
- Error
- Default
+ {#each ["default", "primary", "success", "warning", "error", "info"] as v}
+ {v.charAt(0).toUpperCase() + v.slice(1)}
+ {/each}
+
+
+
+
Sizes
+
+ {#each ["sm", "md", "lg"] as size}
+ {size.toUpperCase()}
+ {/each}
-
-
- List Items
-
-
-
Default Item
-
Hover State
-
Active Item
-
Documents
-
Dashboard
+
+
+ Chip
+
+ {#each ["primary", "success", "warning", "error", "default"] as v}
+ {v.charAt(0).toUpperCase() + v.slice(1)}
+ {/each}
-
-
- Organization Header
+
+
+ StatusBadge
+
+ {#each ["planning", "active", "completed", "archived", "draft"] as status}
+
+ {/each}
+
+
-
+
+
+ Card
+
+ {#each ["default", "elevated", "outlined"] as v}
+
+ {v.charAt(0).toUpperCase() + v.slice(1)}
+ Card content goes here.
+
+ {/each}
+
+
+
+
+
+ SectionCard
+
+
+ Section content with title.
+
+
+ Without icon.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skeleton
+
+
+
Text (3 lines)
+
+
+
+
+
+
+
+
+ Modal
+ (modalOpen = true)}>Open Modal
+
+
(modalOpen = false)} title="Example Modal">
+ Modal dialog content goes here.
+
+ (modalOpen = false)}>Cancel
+ (modalOpen = false)}>Confirm
+
+
+
+
+
+ Toast
+
+ {#each ["success", "error", "warning", "info"] as v}
+
+ {/each}
+
+
+
+
+
+ Logo
+
+ {#each ["sm", "md"] as size}
+
+
+ {size}
+
+ {/each}
+
+
+
+
+
+ ListItem
+
+ Default
+ Hover
+ Active
+ Documents
+
+
+
+
+
-
-
- Calendar Day
+
+
-
+
+
+
+
+
+
+
+
+ CalendarDay
+
-
+
- {#snippet events()}
- Meeting
- {/snippet}
+ {#snippet events()}Meeting{/snippet}
-
-
- Badges
+
+
+ TabBar
+ (activeTab = v)}
+ />
+ Active: {activeTab}
+
-
-
-
Variants
-
- Default
- Primary
- Success
- Warning
- Error
- Info
-
-
-
-
-
Sizes
-
- Small
- Medium
- Large
-
-
+
+
-
-
- Cards
-
-
-
- Default Card
-
- This is a default card with medium padding.
-
-
-
- Elevated Card
-
- This card has a shadow for elevation.
-
-
-
- Outlined Card
-
- This card has a subtle border.
-
-
-
-
-
-
-
- Toggle
-
-
-
-
Sizes
-
-
-
- Small
-
-
-
- Medium
-
-
-
- Large
-
-
-
-
-
-
States
-
-
-
- Off
-
-
-
- On
-
-
-
- Disabled
-
-
-
-
-
-
-
-
-
-
-
- Modal
-
-
- (modalOpen = true)}>Open Modal
-
-
-
-
(modalOpen = false)}
- title="Example Modal"
- >
-
- This is an example modal dialog. You can put any content here.
-
-
- (modalOpen = false)}
- >Cancel
- (modalOpen = false)}>Confirm
-
-
-
-
-
- Typography
-
-
-
-
-
Headings — Tilt Warp
-
-
- h1 · 32
-
Heading 1
-
-
- h2 · 28
-
Heading 2
-
-
- h3 · 24
-
Heading 3
-
-
- h4 · 20
-
Heading 4
-
-
- h5 · 16
-
Heading 5
-
-
- h6 · 14
-
Heading 6
-
-
-
-
-
-
-
Button Text — Tilt Warp
-
-
- btn-lg · 20
- Button Large
-
-
- btn-md · 16
- Button Medium
-
-
- btn-sm · 14
- Button Small
-
-
-
-
-
-
-
Body — Work Sans
-
-
-
p · 16
-
- Body text — Lorem ipsum dolor sit amet,
- consectetur adipiscing elit.
-
-
-
-
p-md · 14
-
- Body medium — Used for secondary information and
- descriptions.
-
-
-
-
p-sm · 12
-
- Body small — Used for metadata, timestamps, and
- hints.
-
-
-
-
-
-
-
-
-
-
-
-
- Logo
- Brand logo component with size variants.
-
-
-
- Small
-
-
-
- Medium
-
-
-
-
-
-
- Content Header
- Page header component with avatar, title, action button, and more menu.
-
- {}}
- onMore={() => {}}
- />
- {}} />
-
-
+
+
+ Dropdown
+ Dropdown + DropdownItem compose a context menu. Click the button to toggle.
+
+ {#snippet trigger()}
+
+ more_vert
+ Open Dropdown
+
+ {/snippet}
+ Edit
+ Duplicate
+ Delete
+
+
diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png
new file mode 100644
index 0000000..bcc6ac4
Binary files /dev/null and b/static/apple-touch-icon.png differ
diff --git a/static/favicon-96x96.png b/static/favicon-96x96.png
new file mode 100644
index 0000000..5ac874b
Binary files /dev/null and b/static/favicon-96x96.png differ
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..752b1ba
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/favicon.svg b/static/favicon.svg
new file mode 100644
index 0000000..7135cf5
--- /dev/null
+++ b/static/favicon.svg
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/static/site.webmanifest b/static/site.webmanifest
new file mode 100644
index 0000000..213bce3
--- /dev/null
+++ b/static/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "root",
+ "short_name": "root",
+ "icons": [
+ {
+ "src": "/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "theme_color": "#0a121f",
+ "background_color": "#0a121f",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/static/web-app-manifest-192x192.png b/static/web-app-manifest-192x192.png
new file mode 100644
index 0000000..9d08d18
Binary files /dev/null and b/static/web-app-manifest-192x192.png differ
diff --git a/static/web-app-manifest-512x512.png b/static/web-app-manifest-512x512.png
new file mode 100644
index 0000000..a588ef1
Binary files /dev/null and b/static/web-app-manifest-512x512.png differ
diff --git a/supabase/migrations/026_department_dashboards.sql b/supabase/migrations/026_department_dashboards.sql
new file mode 100644
index 0000000..2b96d2e
--- /dev/null
+++ b/supabase/migrations/026_department_dashboards.sql
@@ -0,0 +1,258 @@
+-- Department Dashboards: composable workspace for event departments
+-- Each department gets a dashboard with configurable module panels
+
+-- ============================================================
+-- 1. Module types enum
+-- ============================================================
+CREATE TYPE module_type AS ENUM (
+ 'kanban',
+ 'files',
+ 'checklist',
+ 'notes',
+ 'schedule',
+ 'contacts'
+);
+
+-- ============================================================
+-- 2. Layout presets enum
+-- ============================================================
+CREATE TYPE layout_preset AS ENUM (
+ 'single',
+ 'split',
+ 'grid',
+ 'focus_sidebar',
+ 'custom'
+);
+
+-- ============================================================
+-- 3. Department Dashboards
+-- ============================================================
+CREATE TABLE department_dashboards (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ layout layout_preset NOT NULL DEFAULT 'split',
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now(),
+ UNIQUE(department_id)
+);
+
+CREATE INDEX idx_dept_dashboards_dept ON department_dashboards(department_id);
+
+-- ============================================================
+-- 4. Dashboard Panels (modules placed on a dashboard)
+-- ============================================================
+CREATE TABLE dashboard_panels (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ dashboard_id UUID NOT NULL REFERENCES department_dashboards(id) ON DELETE CASCADE,
+ module module_type NOT NULL,
+ position INT NOT NULL DEFAULT 0,
+ width TEXT NOT NULL DEFAULT 'half' CHECK (width IN ('full', 'half', 'third', 'two_thirds')),
+ config JSONB DEFAULT '{}',
+ created_at TIMESTAMPTZ DEFAULT now(),
+ UNIQUE(dashboard_id, position)
+);
+
+CREATE INDEX idx_dashboard_panels_dashboard ON dashboard_panels(dashboard_id);
+
+-- ============================================================
+-- 5. Checklists (scoped to department)
+-- ============================================================
+CREATE TABLE department_checklists (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ title TEXT NOT NULL DEFAULT 'Checklist',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_dept_checklists_dept ON department_checklists(department_id);
+
+CREATE TABLE department_checklist_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ checklist_id UUID NOT NULL REFERENCES department_checklists(id) ON DELETE CASCADE,
+ content TEXT NOT NULL,
+ is_completed BOOLEAN NOT NULL DEFAULT false,
+ assigned_to UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ due_date TIMESTAMPTZ,
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_dept_checklist_items_checklist ON department_checklist_items(checklist_id);
+CREATE INDEX idx_dept_checklist_items_assigned ON department_checklist_items(assigned_to);
+
+-- ============================================================
+-- 6. Department Notes (simple rich text notes)
+-- ============================================================
+CREATE TABLE department_notes (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ title TEXT NOT NULL DEFAULT 'Untitled Note',
+ content TEXT DEFAULT '',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_dept_notes_dept ON department_notes(department_id);
+
+-- ============================================================
+-- 7. Add enabled_modules to event_departments
+-- ============================================================
+ALTER TABLE event_departments
+ ADD COLUMN enabled_modules module_type[] NOT NULL DEFAULT ARRAY['kanban'::module_type, 'files'::module_type, 'checklist'::module_type];
+
+-- ============================================================
+-- 8. RLS Policies
+-- ============================================================
+ALTER TABLE department_dashboards ENABLE ROW LEVEL SECURITY;
+ALTER TABLE dashboard_panels ENABLE ROW LEVEL SECURITY;
+ALTER TABLE department_checklists ENABLE ROW LEVEL SECURITY;
+ALTER TABLE department_checklist_items ENABLE ROW LEVEL SECURITY;
+ALTER TABLE department_notes ENABLE ROW LEVEL SECURITY;
+
+-- Helper: check if user is org member for a department
+-- (department → event → org → org_members)
+
+-- Department Dashboards
+CREATE POLICY "Org members can view department dashboards" ON department_dashboards FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_dashboards.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage department dashboards" ON department_dashboards FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_dashboards.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Dashboard Panels
+CREATE POLICY "Org members can view dashboard panels" ON dashboard_panels FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM department_dashboards dd
+ JOIN event_departments ed ON dd.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE dd.id = dashboard_panels.dashboard_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage dashboard panels" ON dashboard_panels FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM department_dashboards dd
+ JOIN event_departments ed ON dd.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE dd.id = dashboard_panels.dashboard_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Department Checklists
+CREATE POLICY "Org members can view department checklists" ON department_checklists FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_checklists.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage department checklists" ON department_checklists FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_checklists.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Department Checklist Items
+CREATE POLICY "Org members can view dept checklist items" ON department_checklist_items FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM department_checklists dc
+ JOIN event_departments ed ON dc.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE dc.id = department_checklist_items.checklist_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage dept checklist items" ON department_checklist_items FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM department_checklists dc
+ JOIN event_departments ed ON dc.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE dc.id = department_checklist_items.checklist_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Department Notes
+CREATE POLICY "Org members can view department notes" ON department_notes FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_notes.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage department notes" ON department_notes FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = department_notes.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- ============================================================
+-- 9. Enable realtime
+-- ============================================================
+ALTER PUBLICATION supabase_realtime ADD TABLE department_checklists;
+ALTER PUBLICATION supabase_realtime ADD TABLE department_checklist_items;
+ALTER PUBLICATION supabase_realtime ADD TABLE department_notes;
+ALTER PUBLICATION supabase_realtime ADD TABLE dashboard_panels;
+
+-- ============================================================
+-- 10. Auto-create dashboard when department is created
+-- ============================================================
+CREATE OR REPLACE FUNCTION public.create_department_dashboard()
+RETURNS TRIGGER AS $$
+DECLARE
+ dash_id UUID;
+ mod module_type;
+ pos INT := 0;
+BEGIN
+ -- Create dashboard
+ INSERT INTO public.department_dashboards (department_id, layout)
+ VALUES (NEW.id, 'split')
+ RETURNING id INTO dash_id;
+
+ -- Create panels for each enabled module
+ FOREACH mod IN ARRAY NEW.enabled_modules LOOP
+ INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
+ VALUES (dash_id, mod, pos, CASE WHEN pos = 0 THEN 'half' ELSE 'half' END);
+ pos := pos + 1;
+ END LOOP;
+
+ -- Auto-create a default checklist if checklist module is enabled
+ IF 'checklist' = ANY(NEW.enabled_modules) THEN
+ INSERT INTO public.department_checklists (department_id, title, sort_order)
+ VALUES (NEW.id, 'General', 0);
+ END IF;
+
+ -- Auto-create a default note if notes module is enabled
+ IF 'notes' = ANY(NEW.enabled_modules) THEN
+ INSERT INTO public.department_notes (department_id, title, content, sort_order)
+ VALUES (NEW.id, 'Meeting Notes', '', 0);
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE TRIGGER on_department_created_setup_dashboard
+ AFTER INSERT ON event_departments
+ FOR EACH ROW EXECUTE FUNCTION public.create_department_dashboard();
diff --git a/supabase/migrations/027_remove_default_seeds.sql b/supabase/migrations/027_remove_default_seeds.sql
new file mode 100644
index 0000000..4bef019
--- /dev/null
+++ b/supabase/migrations/027_remove_default_seeds.sql
@@ -0,0 +1,11 @@
+-- Remove auto-seeding of departments and roles on event creation.
+-- These will now be offered as suggestions in the UI instead.
+
+-- Drop the existing trigger and function
+DROP TRIGGER IF EXISTS on_event_created_seed_defaults ON events;
+DROP FUNCTION IF EXISTS public.seed_event_defaults();
+
+-- Delete all existing seeded departments and roles
+-- (cascades will clean up member-department assignments, dashboards, panels, checklists, notes)
+DELETE FROM event_departments;
+DELETE FROM event_roles;
diff --git a/supabase/migrations/028_schedule_and_contacts.sql b/supabase/migrations/028_schedule_and_contacts.sql
new file mode 100644
index 0000000..371fa20
--- /dev/null
+++ b/supabase/migrations/028_schedule_and_contacts.sql
@@ -0,0 +1,214 @@
+-- Schedule/Timeline + Contacts/Vendor Directory for department dashboards
+-- These are self-contained modules that can be added to any department dashboard
+
+-- ============================================================
+-- 1. Schedule Stages (rooms/areas where things happen)
+-- ============================================================
+CREATE TABLE schedule_stages (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ name TEXT NOT NULL,
+ color TEXT DEFAULT '#6366f1',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- ============================================================
+-- 2. Schedule Blocks (time blocks in the program)
+-- ============================================================
+CREATE TABLE schedule_blocks (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ stage_id UUID REFERENCES schedule_stages(id) ON DELETE SET NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ start_time TIMESTAMPTZ NOT NULL,
+ end_time TIMESTAMPTZ NOT NULL,
+ color TEXT DEFAULT '#6366f1',
+ speaker TEXT,
+ sort_order INT NOT NULL DEFAULT 0,
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- ============================================================
+-- 3. Contacts / Vendor Directory
+-- ============================================================
+CREATE TABLE department_contacts (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ name TEXT NOT NULL,
+ role TEXT,
+ company TEXT,
+ email TEXT,
+ phone TEXT,
+ website TEXT,
+ notes TEXT,
+ category TEXT DEFAULT 'general',
+ color TEXT DEFAULT '#00A3E0',
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- ============================================================
+-- 4. Indexes
+-- ============================================================
+CREATE INDEX idx_schedule_stages_dept ON schedule_stages(department_id);
+CREATE INDEX idx_schedule_blocks_dept ON schedule_blocks(department_id);
+CREATE INDEX idx_schedule_blocks_stage ON schedule_blocks(stage_id);
+CREATE INDEX idx_schedule_blocks_time ON schedule_blocks(start_time, end_time);
+CREATE INDEX idx_department_contacts_dept ON department_contacts(department_id);
+CREATE INDEX idx_department_contacts_category ON department_contacts(category);
+
+-- ============================================================
+-- 5. RLS Policies
+-- ============================================================
+ALTER TABLE schedule_stages ENABLE ROW LEVEL SECURITY;
+ALTER TABLE schedule_blocks ENABLE ROW LEVEL SECURITY;
+ALTER TABLE department_contacts ENABLE ROW LEVEL SECURITY;
+
+-- Schedule stages: org members can read, editors can write
+CREATE POLICY "schedule_stages_select" ON schedule_stages FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_stages.department_id
+ AND om.user_id = auth.uid()
+ )
+ );
+
+CREATE POLICY "schedule_stages_insert" ON schedule_stages FOR INSERT
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_stages.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "schedule_stages_update" ON schedule_stages FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_stages.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "schedule_stages_delete" ON schedule_stages FOR DELETE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_stages.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+-- Schedule blocks: same pattern
+CREATE POLICY "schedule_blocks_select" ON schedule_blocks FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_blocks.department_id
+ AND om.user_id = auth.uid()
+ )
+ );
+
+CREATE POLICY "schedule_blocks_insert" ON schedule_blocks FOR INSERT
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_blocks.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "schedule_blocks_update" ON schedule_blocks FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_blocks.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "schedule_blocks_delete" ON schedule_blocks FOR DELETE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = schedule_blocks.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+-- Contacts: same pattern
+CREATE POLICY "department_contacts_select" ON department_contacts FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = department_contacts.department_id
+ AND om.user_id = auth.uid()
+ )
+ );
+
+CREATE POLICY "department_contacts_insert" ON department_contacts FOR INSERT
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = department_contacts.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "department_contacts_update" ON department_contacts FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = department_contacts.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
+
+CREATE POLICY "department_contacts_delete" ON department_contacts FOR DELETE
+ USING (
+ EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON e.id = ed.event_id
+ JOIN org_members om ON om.org_id = e.org_id
+ WHERE ed.id = department_contacts.department_id
+ AND om.user_id = auth.uid()
+ AND om.role IN ('owner', 'admin', 'editor')
+ )
+ );
diff --git a/supabase/migrations/029_budget_and_sponsors.sql b/supabase/migrations/029_budget_and_sponsors.sql
new file mode 100644
index 0000000..6509f02
--- /dev/null
+++ b/supabase/migrations/029_budget_and_sponsors.sql
@@ -0,0 +1,202 @@
+-- Budget/Finance and Sponsors & Partners modules
+-- Adds new module types and creates tables for both
+
+-- ============================================================
+-- 1. Extend module_type enum
+-- ============================================================
+ALTER TYPE module_type ADD VALUE IF NOT EXISTS 'budget';
+ALTER TYPE module_type ADD VALUE IF NOT EXISTS 'sponsors';
+
+-- ============================================================
+-- 2. Budget Categories
+-- ============================================================
+CREATE TABLE budget_categories (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ name TEXT NOT NULL,
+ color TEXT DEFAULT '#6366f1',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_budget_categories_dept ON budget_categories(department_id);
+
+-- ============================================================
+-- 3. Budget Items (income or expense line items)
+-- ============================================================
+CREATE TABLE budget_items (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ category_id UUID REFERENCES budget_categories(id) ON DELETE SET NULL,
+ description TEXT NOT NULL,
+ item_type TEXT NOT NULL DEFAULT 'expense' CHECK (item_type IN ('income', 'expense')),
+ planned_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
+ actual_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
+ notes TEXT,
+ sort_order INT NOT NULL DEFAULT 0,
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_budget_items_dept ON budget_items(department_id);
+CREATE INDEX idx_budget_items_category ON budget_items(category_id);
+
+-- ============================================================
+-- 4. Sponsor Tiers (e.g. Platinum, Gold, Silver)
+-- ============================================================
+CREATE TABLE sponsor_tiers (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ name TEXT NOT NULL,
+ amount NUMERIC(12,2) DEFAULT 0,
+ color TEXT DEFAULT '#F59E0B',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_sponsor_tiers_dept ON sponsor_tiers(department_id);
+
+-- ============================================================
+-- 5. Sponsors
+-- ============================================================
+CREATE TABLE sponsors (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
+ tier_id UUID REFERENCES sponsor_tiers(id) ON DELETE SET NULL,
+ name TEXT NOT NULL,
+ contact_name TEXT,
+ contact_email TEXT,
+ contact_phone TEXT,
+ website TEXT,
+ logo_url TEXT,
+ status TEXT NOT NULL DEFAULT 'prospect' CHECK (status IN ('prospect', 'contacted', 'confirmed', 'declined', 'active')),
+ amount NUMERIC(12,2) DEFAULT 0,
+ notes TEXT,
+ created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_sponsors_dept ON sponsors(department_id);
+CREATE INDEX idx_sponsors_tier ON sponsors(tier_id);
+CREATE INDEX idx_sponsors_status ON sponsors(status);
+
+-- ============================================================
+-- 6. Sponsor Deliverables (what we owe each sponsor)
+-- ============================================================
+CREATE TABLE sponsor_deliverables (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ sponsor_id UUID NOT NULL REFERENCES sponsors(id) ON DELETE CASCADE,
+ description TEXT NOT NULL,
+ is_completed BOOLEAN NOT NULL DEFAULT false,
+ due_date TIMESTAMPTZ,
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+CREATE INDEX idx_sponsor_deliverables_sponsor ON sponsor_deliverables(sponsor_id);
+
+-- ============================================================
+-- 7. RLS Policies
+-- ============================================================
+ALTER TABLE budget_categories ENABLE ROW LEVEL SECURITY;
+ALTER TABLE budget_items ENABLE ROW LEVEL SECURITY;
+ALTER TABLE sponsor_tiers ENABLE ROW LEVEL SECURITY;
+ALTER TABLE sponsors ENABLE ROW LEVEL SECURITY;
+ALTER TABLE sponsor_deliverables ENABLE ROW LEVEL SECURITY;
+
+-- Budget Categories
+CREATE POLICY "Org members can view budget categories" ON budget_categories FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = budget_categories.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage budget categories" ON budget_categories FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = budget_categories.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Budget Items
+CREATE POLICY "Org members can view budget items" ON budget_items FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = budget_items.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage budget items" ON budget_items FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = budget_items.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Sponsor Tiers
+CREATE POLICY "Org members can view sponsor tiers" ON sponsor_tiers FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = sponsor_tiers.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage sponsor tiers" ON sponsor_tiers FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = sponsor_tiers.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Sponsors
+CREATE POLICY "Org members can view sponsors" ON sponsors FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = sponsors.department_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage sponsors" ON sponsors FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM event_departments ed
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE ed.id = sponsors.department_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- Sponsor Deliverables
+CREATE POLICY "Org members can view sponsor deliverables" ON sponsor_deliverables FOR SELECT
+ USING (EXISTS (
+ SELECT 1 FROM sponsors s
+ JOIN event_departments ed ON s.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE s.id = sponsor_deliverables.sponsor_id AND om.user_id = auth.uid()
+ ));
+
+CREATE POLICY "Editors can manage sponsor deliverables" ON sponsor_deliverables FOR ALL
+ USING (EXISTS (
+ SELECT 1 FROM sponsors s
+ JOIN event_departments ed ON s.department_id = ed.id
+ JOIN events e ON ed.event_id = e.id
+ JOIN org_members om ON e.org_id = om.org_id
+ WHERE s.id = sponsor_deliverables.sponsor_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor')
+ ));
+
+-- ============================================================
+-- 8. Enable realtime
+-- ============================================================
+ALTER PUBLICATION supabase_realtime ADD TABLE budget_items;
+ALTER PUBLICATION supabase_realtime ADD TABLE sponsors;
+ALTER PUBLICATION supabase_realtime ADD TABLE sponsor_deliverables;
diff --git a/supabase/migrations/030_platform_admin.sql b/supabase/migrations/030_platform_admin.sql
new file mode 100644
index 0000000..f6797bf
--- /dev/null
+++ b/supabase/migrations/030_platform_admin.sql
@@ -0,0 +1,8 @@
+-- Platform admin flag on profiles
+-- Only platform admins can access the /admin dashboard
+
+ALTER TABLE profiles
+ ADD COLUMN is_platform_admin BOOLEAN NOT NULL DEFAULT false;
+
+-- Set the initial platform admin (update this UUID to match your user)
+-- This will be set via the admin API or manually in the DB
diff --git a/synapse/data/homeserver.db b/synapse/data/homeserver.db
new file mode 100644
index 0000000..9ae2779
Binary files /dev/null and b/synapse/data/homeserver.db differ
diff --git a/synapse/data/homeserver.db-shm b/synapse/data/homeserver.db-shm
new file mode 100644
index 0000000..f3dd59a
Binary files /dev/null and b/synapse/data/homeserver.db-shm differ
diff --git a/synapse/data/homeserver.db-wal b/synapse/data/homeserver.db-wal
new file mode 100644
index 0000000..e6f0326
Binary files /dev/null and b/synapse/data/homeserver.db-wal differ
diff --git a/synapse/data/homeserver.yaml b/synapse/data/homeserver.yaml
new file mode 100644
index 0000000..5dd6d55
--- /dev/null
+++ b/synapse/data/homeserver.yaml
@@ -0,0 +1,40 @@
+# Configuration file for Synapse.
+#
+# This is a YAML file: see [1] for a quick introduction. Note in particular
+# that *indentation is important*: all the elements of a list or dictionary
+# should have the same indentation.
+#
+# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html
+#
+# For more information on how to configure Synapse, including a complete accounting of
+# each option, go to docs/usage/configuration/config_documentation.md or
+# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
+server_name: "localhost"
+pid_file: /data/homeserver.pid
+listeners:
+ - port: 8008
+ resources:
+ - compress: false
+ names:
+ - client
+ - federation
+ tls: false
+ type: http
+ x_forwarded: true
+database:
+ name: sqlite3
+ args:
+ database: /data/homeserver.db
+log_config: "/data/localhost.log.config"
+media_store_path: /data/media_store
+registration_shared_secret: "root-org-synapse-secret-change-me"
+report_stats: false
+macaroon_secret_key: "K3l5o7X-X^R38;PiHAYDE;k-&CiW=2cqcFZbJOR@Q2FT_*KT-y"
+form_secret: "X2@N=bAgk*oHscfP,=pz8Je0Pd.zeQAc4DX-oQtQKK*=SJ6raS"
+signing_key_path: "/data/localhost.signing.key"
+trusted_key_servers:
+ - server_name: "matrix.org"
+enable_registration: true
+enable_registration_without_verification: true
+
+# vim:ft=yaml
\ No newline at end of file
diff --git a/synapse/data/localhost.log.config b/synapse/data/localhost.log.config
new file mode 100644
index 0000000..832f0fa
--- /dev/null
+++ b/synapse/data/localhost.log.config
@@ -0,0 +1,39 @@
+version: 1
+
+formatters:
+ precise:
+
+ format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
+
+
+handlers:
+
+
+ console:
+ class: logging.StreamHandler
+ formatter: precise
+
+loggers:
+ # This is just here so we can leave `loggers` in the config regardless of whether
+ # we configure other loggers below (avoid empty yaml dict error).
+ _placeholder:
+ level: "INFO"
+
+
+
+ synapse.storage.SQL:
+ # beware: increasing this to DEBUG will make synapse log sensitive
+ # information such as access tokens.
+ level: INFO
+
+
+
+
+root:
+ level: INFO
+
+
+ handlers: [console]
+
+
+disable_existing_loggers: false
\ No newline at end of file
diff --git a/synapse/data/localhost.signing.key b/synapse/data/localhost.signing.key
new file mode 100644
index 0000000..d3f0cfa
--- /dev/null
+++ b/synapse/data/localhost.signing.key
@@ -0,0 +1 @@
+ed25519 a_VwZG y2OyVm2VBqYOuqiBYBb0wQ4r8awyVMceG+KIPK4K6HA