Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The admin dashboard at https://admin.uscbia.com plus the workspace home for `@bi
- Phase 3: shipping operations (parcels, shipments, pack-requests, routes, contacts)
- Phase 2: event submissions queue + sponsor management

This is also the **source of truth for the Supabase schema**. All migrations live in `supabase/migrations/` and apply to the live Supabase project (`ujkaregrwrppaehvbahf`) via the Supabase MCP or the Supabase CLI. Edge functions live in `supabase/functions/` (deployed via the Supabase MCP).
This is also the **source of truth for the Supabase schema**. All migrations live in `supabase/migrations/` and apply to the live Supabase project (`ujkaregrwrppaehvbahf`) via the Supabase MCP or the Supabase CLI. Edge functions live in `supabase/functions/` (deployed via the Supabase MCP). The historical DDL for the cross-repo shared tables (students, shipping, squad, sublets, events, user profiles, etc.) originated in bia-roommate and george and is archived for provenance in [`docs/schema-history/`](docs/schema-history/) — this repo is the single migrations owner going forward, with the live DB (the baseline) as the source-of-truth starting state; never re-apply the archived SQL.

Local folder is named `BIA 新生service` for historical reasons. The GitHub repo is `bia-admin`. They are the same thing.

Expand Down
2 changes: 1 addition & 1 deletion bia-admin/components/blog/BlogEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { Eye, XCircle } from "lucide-react";
import { roleAtLeast, type Role } from "@biboyang425/bia-shared";
import { ArticleRenderer } from "@biboyang425/bia-shared/articles";
import { ArticleRenderer } from "@biboyang425/bia-shared/react";
import { toast } from "sonner";

import { Input } from "@/components/ui/input";
Expand Down
135 changes: 135 additions & 0 deletions docs/schema-history/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Schema history archive

Inert, read-only copies of the migration SQL that **other repos** used to create
the tables shared across the BIA platform. These files exist for provenance and
archaeology only.

> **Do not apply anything in this directory.** Nothing here is a runnable
> migration. The canonical, append-only migration history lives in
> [`../../supabase/migrations/`](../../supabase/migrations/); the live Supabase
> project (`ujkaregrwrppaehvbahf`) is the ultimate source of truth.

## Why this exists — the ownership story

The shared Postgres schema was not born in this repo. Historically:

- **bia-roommate** (uscbia.com) owned and migrated most of the web-facing shared
tables: roommate profiles, sublets, the entire shipping stack
(`shipments` / `parcels` / `shipping_notifications` / routes / contacts /
pack requests), squad (`找搭子`) posts, apartment comments/votes, feedback.
- **george** (the agent backend) owned and migrated the agent-facing shared
tables: `students`, `messages`, `events`, `event_submissions`, `reminders`,
`sublets`, campus knowledge, user profiles / heartbeat, pending users.

This repo's own baseline (`00000000000000_baseline.sql`) is deliberately a
**no-op placeholder** — it documents that the live project "already has 39 tables
defined by the uscbia.com repo's migrations + manual setup" and never re-creates
them. So for those shared tables there was, until now, **no single source of DDL
provenance**. This archive closes that gap.

**Going forward, `bia-admin` is the single owner of the migrations directory.**
bia-roommate's and george's `supabase/` directories are being removed in parallel
PRs; all new migrations for shared tables land in
[`../../supabase/migrations/`](../../supabase/migrations/) and are applied to the
live DB from here. The "source of truth" claim is true from this point on, with
the **live database (baseline)** as the starting state and these archived files
as the historical record of how that state was reached.

## Inventory

| Repo | Archived files | Source path (now being deleted) |
| --- | --- | --- |
| bia-roommate | 29 `*.sql` | `bia-roommate/supabase/migrations/` |
| george | 15 `*.sql` | `george/supabase/migrations/` |

## Which archived file created which shared table

### bia-roommate

| Archived file | Tables / objects it created |
| --- | --- |
| `20260101_roommate_profiles.sql` | `roommate_profiles` |
| `20260102_profile_likes.sql` | `profile_likes` |
| `20260103_profile_comments.sql` | `profile_comments` |
| `20260104_sublet_listings.sql` | `sublet_listings` (later merged into `sublets`) |
| `20260105_saved_schedules.sql` | `saved_schedules` |
| `20260401_course_rating.sql` | `course_reviews`, `course_rating_aggregates`, `course_lists` |
| `20260402_squad.sql` | `squad_posts`, `squad_members` |
| `20260414_consolidate_sublets.sql` | merges `sublet_listings` (web) + george's `sublets` into one `sublets` table |
| `20260419_shipping.sql` | `warehouse_addresses`, `shipments`, `parcels`, `parcel_events`, `shipping_notifications` |
| `20260419_shipping_user_link.sql` | adds `students.user_id` (auth.users ↔ students bridge) |
| `20260420_shipping_method_routes_contacts.sql` | `shipping_routes`, `shipping_contacts` |
| `20260420_shipping_sensitive_method.sql` | shipping sensitive-method columns |
| `20260421_shipping_sensitive_route_seed.sql` | seed data for sensitive routes |
| `20260422_admin_indexes.sql` | shipping admin indexes |
| `20260423_admin_parcel_rpcs.sql` | **`admin_patch_parcel`, `admin_attach_parcels_to_shipment`** (see WARNING) |
| `20260424_avatars_bucket.sql` | avatars storage bucket |
| `20260424_feedback.sql` | `feedback` |
| `20260425_roommate_contact_channels.sql` | roommate contact-channel columns |
| `20260426_roommate_profiles_grants.sql` | grants on `roommate_profiles` |
| `20260427_shipment_requests_and_frequency.sql` | `shipment_requests` |
| `20260428_pack_requests_and_qr_bucket.sql` | `pack_requests`, `pack_request_parcels`, QR bucket |
| `20260503_events_source_url_unique.sql` | unique index on `events.source_url` |
| `20260606_parcel_notification_enqueue.sql` | **`enqueue_parcel_notification()`** trigger fn (see WARNING) |
| `20260612_hide_test_roommate_profiles.sql` | test-profile hiding |
| `20260612_roommate_profiles_contact_privacy.sql` | contact-privacy columns |
| `20260612_roommate_profiles_is_test.sql` | `roommate_profiles.is_test` |
| `20260624000000_apartment_comments.sql` | `apartment_comments` |
| `20260624100000_apartment_votes.sql` | `apartment_votes` |
| `20260624120000_apartment_vote_access.sql` | apartment-vote RLS/access |

### george

| Archived file | Tables / objects it created |
| --- | --- |
| `001_george_schema.sql` | `students`, `messages`, `events`, `event_submissions`, `reminders`, `sublets`, `campus_knowledge`, `student_memories`, `event_attendance`, `student_connections`, `proactive_log` |
| `002_courses_programs.sql` | `courses`, `programs` |
| `002_onboarding_tracking.sql` | onboarding-tracking columns on `students` |
| `003_wechat_ingest_knowledge.sql` | `freshman_faq`, `course_tips` |
| `010_user_profiles.sql` | `user_profiles` (FK `students(user_id)` — see inconsistency note) |
| `011_user_heartbeat_config.sql` | `user_heartbeat_config` (FK `students(user_id)`) |
| `012_user_heartbeat_instructions.sql` | `user_heartbeat_instructions` (FK `students(user_id)`) |
| `013_heartbeat_log.sql` | `heartbeat_log` (FK `students(user_id)`) |
| `014_student_followups.sql` | `student_followups` (FK `students(user_id)`) |
| `015_pending_users.sql` | `pending_users` |
| `20260522_extend_memory_categories.sql` | memory-category extension |
| `20260611_messages_user_id_session_memory.sql` | `messages.user_id` + session-memory columns |
| `20260624_user_heartbeat_config_authenticated_insert.sql` | heartbeat-config RLS |
| `20260624_user_heartbeat_config_consent_memory.sql` | heartbeat consent-memory columns |
| `20260625_message_flags.sql` | `message_flags` |

## ⚠️ WARNING — divergent function bodies

Three functions were defined in **both** the roommate archive and this repo's
canonical migrations, and their bodies **diverge**. This repo's canonical
migrations carry the evolved, hardened definitions; the archived versions are
older and would **regress** the live function if applied.

| Function | Archived (stale) definition | Canonical definition — THE ONLY correct source |
| --- | --- | --- |
| `enqueue_parcel_notification()` | `bia-roommate/20260606_parcel_notification_enqueue.sql` | `20260606061321_parcel_notification_enqueue.sql` → `20260606061351_..._branch_states.sql` → `20260611000001_widen_shipping_notification_kinds.sql` → `20260703000004_notification_pipeline_correctness.sql` |
| `admin_patch_parcel(...)` | `bia-roommate/20260423_admin_parcel_rpcs.sql` | `20260624000001_admin_patch_parcel_stamp_received_at.sql` → `20260703000001_shipping_state_machine_hardening.sql` |
| `admin_attach_parcels_to_shipment(...)` | `bia-roommate/20260423_admin_parcel_rpcs.sql` | `20260623000001_attach_rpc_received_cn_guard.sql` → `20260624000002_admin_attach_pack_request_atomic.sql` → `20260624000005_codex_review_shipping_hardening.sql` → `20260703000001_shipping_state_machine_hardening.sql` → `20260703000006_pack_request_approve_on_batch.sql` |

**Only the `supabase/migrations/` definitions are canonical. Never apply the
archived versions of these functions — doing so silently reverts security and
correctness hardening that shipped after the archive was frozen.**

## Note — george's local set was internally inconsistent

The george archive is **provenance, not a runnable history**. Its own migrations
do not form a consistent chain:

- `001_george_schema.sql` creates `students` keyed on `id uuid PRIMARY KEY`
(plus platform IDs `wechat_open_id` / `imessage_id`). It has **no `user_id`
column**.
- `010_user_profiles.sql` through `014_student_followups.sql` all declare
`user_id uuid ... REFERENCES students(user_id)` — a foreign key against a
column george's own `001` never created.

That `students.user_id` column was actually introduced by **bia-roommate's**
`20260419_shipping_user_link.sql` (the auth.users ↔ students bridge). So george's
010–014 migrations only apply cleanly on a database that had already run
roommate's shipping-user-link migration. Running the george set in isolation from
`001` would fail. Treat these files as a record of intent, not a replayable
sequence — reconstruct real history from the live DB, not from this archive.
51 changes: 51 additions & 0 deletions docs/schema-history/bia-roommate/20260101_roommate_profiles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- Roommate Profiles: core table for roommate matching
-- Apply via Supabase dashboard SQL editor or supabase db push

-- ─── Table: roommate_profiles ───
CREATE TABLE IF NOT EXISTS roommate_profiles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
name text NOT NULL,
school text NOT NULL CHECK (school IN ('USC', 'UC Berkeley', 'Stanford')),
gender text,
major text,
year text,
enrollment_term text,
contact text NOT NULL,
sleep_habit text,
clean_level text,
noise_level text,
music_habit text,
study_style text,
hobbies text,
tags text[],
avatar_url text,
bio text,
visible boolean DEFAULT false,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);

-- ─── Indexes ───
CREATE INDEX idx_roommate_profiles_school ON roommate_profiles (school);
CREATE INDEX idx_roommate_profiles_user_id ON roommate_profiles (user_id);
CREATE INDEX idx_roommate_profiles_visible_school ON roommate_profiles (visible, school);

-- ─── RLS Policies ───
ALTER TABLE roommate_profiles ENABLE ROW LEVEL SECURITY;

-- Anyone can read visible profiles (public browse)
CREATE POLICY "read_visible_profiles" ON roommate_profiles FOR SELECT
USING (visible = true);

-- Authenticated users can create profiles
CREATE POLICY "insert_own_profile" ON roommate_profiles FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);

-- Users can update their own profile
CREATE POLICY "update_own_profile" ON roommate_profiles FOR UPDATE
USING (auth.uid() = user_id);

-- Users can delete their own profile
CREATE POLICY "delete_own_profile" ON roommate_profiles FOR DELETE
USING (auth.uid() = user_id);
30 changes: 30 additions & 0 deletions docs/schema-history/bia-roommate/20260102_profile_likes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Profile Likes: tracks which users liked which roommate profiles
-- Apply via Supabase dashboard SQL editor or supabase db push

-- ─── Table: profile_likes ───
CREATE TABLE IF NOT EXISTS profile_likes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES roommate_profiles(id) ON DELETE CASCADE,
created_at timestamptz DEFAULT now(),
UNIQUE (user_id, profile_id)
);

-- ─── Indexes ───
-- profile_id index for fast like-count queries
CREATE INDEX idx_profile_likes_profile_id ON profile_likes (profile_id);

-- ─── RLS Policies ───
ALTER TABLE profile_likes ENABLE ROW LEVEL SECURITY;

-- Anyone can read likes (public counts)
CREATE POLICY "read_likes" ON profile_likes FOR SELECT
USING (true);

-- Authenticated users can like profiles (insert own rows)
CREATE POLICY "insert_own_like" ON profile_likes FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can remove their own likes
CREATE POLICY "delete_own_like" ON profile_likes FOR DELETE
USING (auth.uid() = user_id);
30 changes: 30 additions & 0 deletions docs/schema-history/bia-roommate/20260103_profile_comments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Profile Comments: user comments on roommate profiles
-- Apply via Supabase dashboard SQL editor or supabase db push

-- ─── Table: profile_comments ───
CREATE TABLE IF NOT EXISTS profile_comments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
profile_id uuid NOT NULL REFERENCES roommate_profiles(id) ON DELETE CASCADE,
content text NOT NULL CHECK (char_length(content) > 0),
created_at timestamptz DEFAULT now()
);

-- ─── Indexes ───
-- Composite index for fetching comments on a profile sorted by time
CREATE INDEX idx_profile_comments_profile_created ON profile_comments (profile_id, created_at DESC);

-- ─── RLS Policies ───
ALTER TABLE profile_comments ENABLE ROW LEVEL SECURITY;

-- Anyone can read comments (public)
CREATE POLICY "read_comments" ON profile_comments FOR SELECT
USING (true);

-- Authenticated users can post comments
CREATE POLICY "insert_own_comment" ON profile_comments FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can delete their own comments
CREATE POLICY "delete_own_comment" ON profile_comments FOR DELETE
USING (auth.uid() = user_id);
48 changes: 48 additions & 0 deletions docs/schema-history/bia-roommate/20260104_sublet_listings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Sublet Listings: housing sublet posts by school
-- Apply via Supabase dashboard SQL editor or supabase db push

-- ─── Table: sublet_listings ───
CREATE TABLE IF NOT EXISTS sublet_listings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title text NOT NULL,
apartment_name text NOT NULL,
address text NOT NULL,
school text NOT NULL CHECK (school IN ('USC', 'UC Berkeley', 'Stanford')),
rent integer NOT NULL CHECK (rent > 0),
room_type text,
bathrooms text,
move_in_date date,
move_out_date date,
gender_preference text,
description text,
amenities text[],
photos text[],
contact text NOT NULL,
poster_name text NOT NULL,
created_at timestamptz DEFAULT now()
);

-- ─── Indexes ───
CREATE INDEX idx_sublet_listings_school ON sublet_listings (school);
CREATE INDEX idx_sublet_listings_user_id ON sublet_listings (user_id);
CREATE INDEX idx_sublet_listings_created_at ON sublet_listings (created_at DESC);

-- ─── RLS Policies ───
ALTER TABLE sublet_listings ENABLE ROW LEVEL SECURITY;

-- Anyone can browse listings (public)
CREATE POLICY "read_listings" ON sublet_listings FOR SELECT
USING (true);

-- Authenticated users can create listings
CREATE POLICY "insert_own_listing" ON sublet_listings FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can update their own listings
CREATE POLICY "update_own_listing" ON sublet_listings FOR UPDATE
USING (auth.uid() = user_id);

-- Users can delete their own listings
CREATE POLICY "delete_own_listing" ON sublet_listings FOR DELETE
USING (auth.uid() = user_id);
37 changes: 37 additions & 0 deletions docs/schema-history/bia-roommate/20260105_saved_schedules.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Saved Schedules: user-saved course schedules with preferences
-- Apply via Supabase dashboard SQL editor or supabase db push

-- ─── Table: saved_schedules ───
CREATE TABLE IF NOT EXISTS saved_schedules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name text NOT NULL,
semester text NOT NULL,
courses jsonb NOT NULL DEFAULT '[]',
preferences jsonb DEFAULT '{}',
schedule_data jsonb NOT NULL DEFAULT '[]',
created_at timestamptz DEFAULT now()
);

-- ─── Indexes ───
-- Composite index for fetching a user's schedules sorted by time
CREATE INDEX idx_saved_schedules_user_created ON saved_schedules (user_id, created_at DESC);

-- ─── RLS Policies ───
ALTER TABLE saved_schedules ENABLE ROW LEVEL SECURITY;

-- Users can only read their own schedules
CREATE POLICY "read_own_schedules" ON saved_schedules FOR SELECT
USING (auth.uid() = user_id);

-- Users can create their own schedules
CREATE POLICY "insert_own_schedule" ON saved_schedules FOR INSERT
WITH CHECK (auth.uid() = user_id);

-- Users can update their own schedules
CREATE POLICY "update_own_schedule" ON saved_schedules FOR UPDATE
USING (auth.uid() = user_id);

-- Users can delete their own schedules
CREATE POLICY "delete_own_schedule" ON saved_schedules FOR DELETE
USING (auth.uid() = user_id);
Loading
Loading