Show the SQL (830 lines)
-- RareLink initial schema. Run against a Supabase Postgres database.
-- Convention: all amounts in cents (integer). UUIDs are generated by Supabase Auth or gen_random_uuid().
create extension if not exists "pgcrypto";
create extension if not exists "citext";
-- ============================================================================
-- ENUMS
-- ============================================================================
create type order_status as enum (
'pending',
'paid',
'awaiting_shipment',
'shipped',
'delivered',
'inspection_window',
'payout_ready',
'payout_released',
'disputed',
'refunded',
'cancelled'
);
create type listing_status as enum (
'draft',
'active',
'sold',
'removed'
);
create type listing_game as enum (
'pokemon',
'magic',
'yugioh',
'one_piece',
'sports',
'other'
);
create type listing_raw_graded as enum ('raw', 'graded');
create type photo_type as enum (
'front',
'back',
'corner',
'surface',
'edge',
'slab_cert',
'packing',
'other'
);
create type dispute_status as enum (
'open',
'evidence_pending',
'admin_review',
'resolved_buyer',
'resolved_seller',
'resolved_split'
);
create type dispute_type as enum (
'not_received',
'not_as_described',
'counterfeit',
'damaged',
'other'
);
create type evidence_type as enum (
'photo',
'video',
'receipt',
'conversation',
'tracking',
'other'
);
create type trust_event_type as enum (
'id_verified',
'first_sale',
'on_time_ship',
'buyer_accepted',
'on_time_no_dispute',
'packing_proof',
'late_ship',
'cancelled_paid',
'dispute_lost',
'wrong_item',
'fraud_flag',
'chargeback',
'admin_adjustment'
);
create type user_role as enum ('buyer', 'seller', 'both');
create type verification_status as enum ('unverified', 'pending', 'verified', 'disabled');
-- ============================================================================
-- TABLES
-- ============================================================================
create table users (
id uuid primary key default gen_random_uuid(),
email citext not null unique,
username text unique,
role user_role not null default 'buyer',
is_admin boolean not null default false,
stripe_account_id text unique,
verification_status verification_status not null default 'unverified',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
last_sign_in_at timestamptz
);
create table seller_profiles (
id uuid primary key default gen_random_uuid(),
user_id uuid not null unique references users(id) on delete restrict,
display_name text not null,
bio text,
avatar_url text,
trust_score smallint not null default 50 check (trust_score between 0 and 100),
completed_sales integer not null default 0,
dispute_count integer not null default 0,
refund_count integer not null default 0,
avg_ship_hours numeric(8, 2),
is_verified boolean not null default false,
counters_updated_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table listings (
id uuid primary key default gen_random_uuid(),
seller_id uuid not null references users(id) on delete restrict,
title text not null,
game listing_game not null default 'pokemon',
set_name text,
card_number text,
rarity text,
raw_or_graded listing_raw_graded not null default 'raw',
grading_company text,
grade text,
cert_number text,
condition text,
description text,
price_cents integer not null check (price_cents >= 0),
shipping_cents integer not null default 0 check (shipping_cents >= 0),
quantity smallint not null default 1 check (quantity = 1), -- P1 enforces 1; column reserved for P2.
status listing_status not null default 'draft',
public_slug text not null unique,
view_count integer not null default 0,
sold_order_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table listing_photos (
id uuid primary key default gen_random_uuid(),
listing_id uuid not null references listings(id) on delete cascade,
photo_url text not null,
photo_type photo_type not null default 'front',
width integer,
height integer,
bytes integer,
sort_order smallint not null default 0,
created_at timestamptz not null default now()
);
create table orders (
id uuid primary key default gen_random_uuid(),
listing_id uuid not null references listings(id) on delete restrict,
seller_id uuid not null references users(id) on delete restrict,
buyer_id uuid not null references users(id) on delete restrict,
status order_status not null default 'pending',
currency char(3) not null default 'USD',
item_price_cents integer not null check (item_price_cents >= 0),
shipping_cents integer not null default 0 check (shipping_cents >= 0),
platform_fee_cents integer not null check (platform_fee_cents >= 0),
buyer_protection_fee_cents integer not null check (buyer_protection_fee_cents >= 0),
processing_fee_estimate_cents integer not null check (processing_fee_estimate_cents >= 0),
seller_net_cents integer not null check (seller_net_cents >= 0),
stripe_payment_intent_id text unique,
stripe_charge_id text,
stripe_transfer_id text,
stripe_refund_id text,
stripe_dispute_id text,
payment_method_type text,
tracking_number text,
shipping_carrier text,
shipping_deadline timestamptz,
shipped_at timestamptz,
delivered_at timestamptz,
inspection_deadline timestamptz,
buyer_accepted_at timestamptz,
payout_released_at timestamptz,
cancelled_at timestamptz,
cancellation_reason text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint orders_inspection_after_delivered
check (inspection_deadline is null or delivered_at is null or inspection_deadline > delivered_at)
);
alter table listings
add constraint listings_sold_order_fk
foreign key (sold_order_id) references orders(id) on delete set null;
create table disputes (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references orders(id) on delete restrict,
buyer_id uuid not null references users(id) on delete restrict,
seller_id uuid not null references users(id) on delete restrict,
dispute_type dispute_type not null default 'other',
reason text,
description text,
status dispute_status not null default 'open',
admin_decision text,
refund_amount_cents integer check (refund_amount_cents is null or refund_amount_cents >= 0),
opened_at timestamptz not null default now(),
resolved_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table dispute_evidence (
id uuid primary key default gen_random_uuid(),
dispute_id uuid not null references disputes(id) on delete cascade,
uploaded_by uuid not null references users(id) on delete restrict,
file_url text not null,
evidence_type evidence_type not null default 'photo',
note text,
created_at timestamptz not null default now()
);
create table messages (
id uuid primary key default gen_random_uuid(),
order_id uuid not null references orders(id) on delete restrict,
sender_id uuid not null references users(id) on delete restrict,
message text not null,
read_at timestamptz,
created_at timestamptz not null default now()
);
create table audit_logs (
id uuid primary key default gen_random_uuid(),
actor_id uuid references users(id) on delete set null,
entity_type text not null,
entity_id uuid not null,
action text not null,
metadata_json jsonb not null default '{}'::jsonb,
ip_address inet,
user_agent text,
created_at timestamptz not null default now()
);
create table trust_events (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete restrict,
order_id uuid references orders(id) on delete set null,
event_type trust_event_type not null,
points_change smallint not null,
reason text,
created_at timestamptz not null default now()
);
-- Row Level Security policies for RareLink.
-- Apply AFTER 0001_init.sql. All tables get RLS enabled; all writes that touch
-- money go through service-role (which bypasses RLS). Buyers and sellers can
-- only directly INSERT messages and dispute_evidence.
-- ============================================================================
-- USERS
-- ============================================================================
alter table users enable row level security;
create policy users_select_self
on users for select
using (auth.uid() = id);
create policy users_select_admin
on users for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
create policy users_update_self_limited
on users for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- ============================================================================
-- SELLER PROFILES
-- ============================================================================
alter table seller_profiles enable row level security;
create policy seller_profiles_select_public
on seller_profiles for select
using (true); -- public display_name, bio, avatar, trust_score. Sensitive cols not stored here.
create policy seller_profiles_update_self
on seller_profiles for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- ============================================================================
-- LISTINGS
-- ============================================================================
alter table listings enable row level security;
create policy listings_select_active
on listings for select
using (status = 'active');
create policy listings_select_owner
on listings for select
using (auth.uid() = seller_id);
create policy listings_select_admin
on listings for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
create policy listings_insert_owner
on listings for insert
with check (
auth.uid() = seller_id
and exists (
select 1 from users u
where u.id = auth.uid() and u.stripe_account_id is not null
)
);
create policy listings_update_owner
on listings for update
using (auth.uid() = seller_id and status in ('draft', 'active'))
with check (auth.uid() = seller_id);
create policy listings_delete_owner_draft
on listings for delete
using (auth.uid() = seller_id and status = 'draft');
-- ============================================================================
-- LISTING PHOTOS — inherits from listings
-- ============================================================================
alter table listing_photos enable row level security;
create policy listing_photos_select
on listing_photos for select
using (
exists (
select 1 from listings l
where l.id = listing_photos.listing_id
and (l.status = 'active' or l.seller_id = auth.uid())
)
);
create policy listing_photos_insert_owner
on listing_photos for insert
with check (
exists (
select 1 from listings l
where l.id = listing_photos.listing_id
and l.seller_id = auth.uid()
)
);
create policy listing_photos_delete_owner
on listing_photos for delete
using (
exists (
select 1 from listings l
where l.id = listing_photos.listing_id
and l.seller_id = auth.uid()
)
);
-- ============================================================================
-- ORDERS — service-role only for INSERT/UPDATE. Participants can SELECT.
-- ============================================================================
alter table orders enable row level security;
create policy orders_select_participant
on orders for select
using (auth.uid() = buyer_id or auth.uid() = seller_id);
create policy orders_select_admin
on orders for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
-- No INSERT/UPDATE/DELETE policies — service-role bypass only.
-- ============================================================================
-- DISPUTES
-- ============================================================================
alter table disputes enable row level security;
create policy disputes_select_participant
on disputes for select
using (auth.uid() = buyer_id or auth.uid() = seller_id);
create policy disputes_select_admin
on disputes for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
create policy disputes_insert_buyer
on disputes for insert
with check (
auth.uid() = buyer_id
and exists (
select 1 from orders o
where o.id = disputes.order_id and o.buyer_id = auth.uid()
)
);
-- ============================================================================
-- DISPUTE EVIDENCE
-- ============================================================================
alter table dispute_evidence enable row level security;
create policy dispute_evidence_select_participant
on dispute_evidence for select
using (
exists (
select 1 from disputes d
where d.id = dispute_evidence.dispute_id
and (d.buyer_id = auth.uid() or d.seller_id = auth.uid())
)
);
create policy dispute_evidence_insert_participant
on dispute_evidence for insert
with check (
uploaded_by = auth.uid()
and exists (
select 1 from disputes d
where d.id = dispute_evidence.dispute_id
and (d.buyer_id = auth.uid() or d.seller_id = auth.uid())
)
);
-- ============================================================================
-- MESSAGES
-- ============================================================================
alter table messages enable row level security;
create policy messages_select_participant
on messages for select
using (
exists (
select 1 from orders o
where o.id = messages.order_id
and (o.buyer_id = auth.uid() or o.seller_id = auth.uid())
)
);
create policy messages_insert_participant
on messages for insert
with check (
sender_id = auth.uid()
and exists (
select 1 from orders o
where o.id = messages.order_id
and (o.buyer_id = auth.uid() or o.seller_id = auth.uid())
)
);
-- ============================================================================
-- AUDIT LOGS — admin select only. Service-role inserts.
-- ============================================================================
alter table audit_logs enable row level security;
create policy audit_logs_select_admin
on audit_logs for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
-- ============================================================================
-- TRUST EVENTS
-- ============================================================================
alter table trust_events enable row level security;
create policy trust_events_select_self
on trust_events for select
using (auth.uid() = user_id);
create policy trust_events_select_admin
on trust_events for select
using (exists (select 1 from users u where u.id = auth.uid() and u.is_admin));
-- Indexes for RareLink. Apply after 0001/0002.
-- Users
create index users_role_idx on users (role);
-- Seller profiles — leaderboard later
create index seller_profiles_trust_idx on seller_profiles (trust_score desc);
-- Listings
create index listings_seller_status_idx on listings (seller_id, status);
create index listings_status_idx on listings (status) where status = 'active';
create index listings_game_idx on listings (game) where status = 'active';
-- Listing photos
create index listing_photos_listing_idx on listing_photos (listing_id, sort_order);
-- Orders — the most query-pressured table
create index orders_seller_status_idx on orders (seller_id, status);
create index orders_buyer_status_idx on orders (buyer_id, status);
-- Cron sweep hot path. Partial index keeps it small.
create index orders_inspection_due_idx
on orders (inspection_deadline)
where status = 'inspection_window';
create index orders_shipping_due_idx
on orders (shipping_deadline)
where status = 'awaiting_shipment';
create index orders_stripe_charge_idx on orders (stripe_charge_id);
create index orders_stripe_dispute_idx on orders (stripe_dispute_id);
-- Disputes — only one active per order
create unique index disputes_one_active_per_order
on disputes (order_id)
where status in ('open', 'evidence_pending', 'admin_review');
create index disputes_status_opened_idx on disputes (status, opened_at);
-- Dispute evidence
create index dispute_evidence_dispute_idx on dispute_evidence (dispute_id, created_at);
-- Messages
create index messages_order_idx on messages (order_id, created_at);
-- Audit logs
create index audit_logs_entity_idx on audit_logs (entity_type, entity_id);
create index audit_logs_actor_idx on audit_logs (actor_id, created_at desc);
-- Trust events
create index trust_events_user_idx on trust_events (user_id, created_at desc);
create unique index trust_events_no_double_credit
on trust_events (user_id, order_id, event_type)
where order_id is not null;
-- Triggers: updated_at maintenance.
create or replace function set_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
create trigger users_updated_at
before update on users
for each row execute function set_updated_at();
create trigger seller_profiles_updated_at
before update on seller_profiles
for each row execute function set_updated_at();
create trigger listings_updated_at
before update on listings
for each row execute function set_updated_at();
create trigger orders_updated_at
before update on orders
for each row execute function set_updated_at();
create trigger disputes_updated_at
before update on disputes
for each row execute function set_updated_at();
-- Migration: trade-offers
-- Adds trade offer support to listings
-- Add trade columns to listings
ALTER TABLE listings ADD COLUMN IF NOT EXISTS accepts_trades boolean NOT NULL DEFAULT false;
ALTER TABLE listings ADD COLUMN IF NOT EXISTS trade_preferences text;
-- Create trade_offers table
CREATE TABLE IF NOT EXISTS trade_offers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
listing_id uuid NOT NULL REFERENCES listings(id) ON DELETE CASCADE,
buyer_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
seller_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Offered card details
offered_card_name text NOT NULL,
offered_card_set text,
offered_card_condition text,
offered_card_photos text[] NOT NULL DEFAULT '{}',
offered_card_description text,
-- Cash component
cash_difference_cents integer NOT NULL DEFAULT 0,
-- Positive = buyer pays extra, Negative = seller pays extra
-- Status
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'rejected', 'cancelled', 'completed')),
-- Messages
buyer_message text,
seller_response text,
-- Timestamps
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
responded_at timestamptz,
completed_at timestamptz
);
-- Indexes for trade_offers
CREATE INDEX IF NOT EXISTS idx_trade_offers_listing ON trade_offers(listing_id);
CREATE INDEX IF NOT EXISTS idx_trade_offers_buyer ON trade_offers(buyer_id);
CREATE INDEX IF NOT EXISTS idx_trade_offers_seller ON trade_offers(seller_id);
CREATE INDEX IF NOT EXISTS idx_trade_offers_status ON trade_offers(status);
-- RLS for trade_offers
ALTER TABLE trade_offers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Trade offers viewable by participants"
ON trade_offers FOR SELECT
TO authenticated
USING (buyer_id = auth.uid() OR seller_id = auth.uid());
CREATE POLICY "Buyers can create trade offers"
ON trade_offers FOR INSERT
TO authenticated
WITH CHECK (buyer_id = auth.uid());
CREATE POLICY "Sellers can update their trade offers"
ON trade_offers FOR UPDATE
TO authenticated
USING (seller_id = auth.uid());
-- Trigger for updated_at
CREATE OR REPLACE FUNCTION update_trade_offers_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_trade_offers_updated_at ON trade_offers;
CREATE TRIGGER update_trade_offers_updated_at
BEFORE UPDATE ON trade_offers
FOR EACH ROW
EXECUTE FUNCTION update_trade_offers_updated_at();
-- Migration 0007 — canonical Pokemon TCG card reference + wishlist + presence index.
-- Apply via Supabase SQL editor after 0006_trade_offers.sql.
-- ============================================================================
-- listings: link to canonical Pokemon TCG card + remembered image
-- ============================================================================
ALTER TABLE listings
ADD COLUMN IF NOT EXISTS tcg_card_id text,
ADD COLUMN IF NOT EXISTS canonical_image_url text,
ADD COLUMN IF NOT EXISTS card_number text;
CREATE INDEX IF NOT EXISTS listings_tcg_card_idx ON listings (tcg_card_id) WHERE tcg_card_id IS NOT NULL;
-- ============================================================================
-- wishlist_items: "Cards I'm Hunting"
-- ============================================================================
CREATE TABLE IF NOT EXISTS wishlist_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Canonical card from Pokemon TCG API (preferred)
tcg_card_id text,
card_name text NOT NULL, -- displayed name even if no canonical id
set_name text,
rarity text,
canonical_image_url text,
-- Price ceiling — "I'd buy this for $X or less"
max_price_cents integer,
-- Condition floor — "only NM/PSA9+"
min_condition text,
-- Activity flags
notify_on_match boolean NOT NULL DEFAULT true,
fulfilled_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
-- A user can't add the same TCG card twice
CONSTRAINT wishlist_items_unique_per_user UNIQUE (user_id, tcg_card_id)
);
CREATE INDEX IF NOT EXISTS wishlist_items_user_idx ON wishlist_items (user_id, created_at desc);
CREATE INDEX IF NOT EXISTS wishlist_items_tcg_active_idx
ON wishlist_items (tcg_card_id)
WHERE fulfilled_at IS NULL AND tcg_card_id IS NOT NULL;
ALTER TABLE wishlist_items ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "wishlist_select_self" ON wishlist_items;
CREATE POLICY "wishlist_select_self"
ON wishlist_items FOR SELECT
USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "wishlist_insert_self" ON wishlist_items;
CREATE POLICY "wishlist_insert_self"
ON wishlist_items FOR INSERT
WITH CHECK (auth.uid() = user_id);
DROP POLICY IF EXISTS "wishlist_update_self" ON wishlist_items;
CREATE POLICY "wishlist_update_self"
ON wishlist_items FOR UPDATE
USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "wishlist_delete_self" ON wishlist_items;
CREATE POLICY "wishlist_delete_self"
ON wishlist_items FOR DELETE
USING (auth.uid() = user_id);
-- ============================================================================
-- wishlist_matches: cached "this wishlist item just appeared as a listing"
-- Written by the match-detection cron — not user-mutable.
-- ============================================================================
CREATE TABLE IF NOT EXISTS wishlist_matches (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
wishlist_item_id uuid NOT NULL REFERENCES wishlist_items(id) ON DELETE CASCADE,
listing_id uuid NOT NULL REFERENCES listings(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
notified_at timestamptz,
viewed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT wishlist_matches_unique UNIQUE (wishlist_item_id, listing_id)
);
CREATE INDEX IF NOT EXISTS wishlist_matches_user_idx ON wishlist_matches (user_id, created_at desc);
ALTER TABLE wishlist_matches ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "wishlist_matches_select_self" ON wishlist_matches;
CREATE POLICY "wishlist_matches_select_self"
ON wishlist_matches FOR SELECT
USING (auth.uid() = user_id);
-- Migration 0008 — pack tracker + collector analytics
-- Apply via Supabase SQL editor after 0007_canonical_card.sql
-- ============================================================================
-- pack_openings — every booster, box, ETB, tin, or pack a user has ripped
-- ============================================================================
CREATE TABLE IF NOT EXISTS pack_openings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- What was opened
product_name text NOT NULL, -- "Chaos Rising Booster Box"
set_name text,
product_type text NOT NULL DEFAULT 'booster_box'
CHECK (product_type IN ('pack', 'booster_box', 'etb', 'collection', 'tin', 'bundle', 'other')),
num_packs smallint NOT NULL DEFAULT 1 CHECK (num_packs BETWEEN 1 AND 144),
-- Acquisition
source text NOT NULL DEFAULT 'retail'
CHECK (source IN ('rarelink', 'retail', 'other_marketplace', 'gift', 'opened_blind')),
order_id uuid REFERENCES orders(id) ON DELETE SET NULL,
cost_cents integer CHECK (cost_cents IS NULL OR cost_cents >= 0),
-- Pulls — denormalized JSONB array of { card_name, set_name, rarity,
-- estimated_value_cents, tcg_card_id (optional), image_url (optional) }
pulls jsonb NOT NULL DEFAULT '[]'::jsonb,
total_pull_value_cents integer NOT NULL DEFAULT 0 CHECK (total_pull_value_cents >= 0),
-- Visibility
is_public boolean NOT NULL DEFAULT false,
-- Free-text notes
notes text,
-- When
opened_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS pack_openings_user_idx ON pack_openings (user_id, opened_at DESC);
CREATE INDEX IF NOT EXISTS pack_openings_order_idx ON pack_openings (order_id) WHERE order_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS pack_openings_public_idx ON pack_openings (opened_at DESC) WHERE is_public = true;
ALTER TABLE pack_openings ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "pack_openings_select_self" ON pack_openings;
CREATE POLICY "pack_openings_select_self"
ON pack_openings FOR SELECT
USING (auth.uid() = user_id);
DROP POLICY IF EXISTS "pack_openings_select_public" ON pack_openings;
CREATE POLICY "pack_openings_select_public"
ON pack_openings FOR SELECT
USING (is_public = true);
DROP POLICY IF EXISTS "pack_openings_insert_self" ON pack_openings;
CREATE POLICY "pack_openings_insert_self"
ON pack_openings FOR INSERT
WITH CHECK (auth.uid() = user_id);
DROP POLICY IF EXISTS "pack_openings_update_self" ON pack_openings;
CREATE POLICY "pack_openings_update_self"
ON pack_openings FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
DROP POLICY IF EXISTS "pack_openings_delete_self" ON pack_openings;
CREATE POLICY "pack_openings_delete_self"
ON pack_openings FOR DELETE
USING (auth.uid() = user_id);
-- Trigger to keep updated_at fresh
CREATE OR REPLACE FUNCTION update_pack_openings_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_pack_openings_updated_at ON pack_openings;
CREATE TRIGGER update_pack_openings_updated_at
BEFORE UPDATE ON pack_openings
FOR EACH ROW
EXECUTE FUNCTION update_pack_openings_updated_at();