| name | seed-club |
| description | Provision a complete demo Club graph into the local/dev Supabase for Phase-8 manual QA and Playwright e2e — 1 Club, 1 Admin, 1 Staff, 4 Members across 2 Membership Types, tee-sheet settings, sample Bookings (including a Guest with marketing consent), and one Block. |
| disable-model-invocation | true |
Seed Club — QA Fixtures
Provisions a deterministic demo Club into the local Supabase instance. Safe to re-run — tears down any previous seed before re-seeding.
Never run against production. The script targets the local Supabase Admin API only.
Prerequisites
- Local Supabase is running (
supabase start). If not, run /setup-dev first.
curl and jq are available in your shell.
- No extra secrets needed — everything is read from
supabase status.
Step 1 — Capture local Supabase credentials
SUPA_STATUS=$(supabase status -o json)
API_URL=$(echo "$SUPA_STATUS" | jq -r '.API_URL')
SERVICE_KEY=$(echo "$SUPA_STATUS" | jq -r '.SERVICE_ROLE_KEY')
DB_URL=$(echo "$SUPA_STATUS" | jq -r '.DB_URL')
Verify the values are non-empty:
echo "API_URL=$API_URL"
echo "SERVICE_KEY=${SERVICE_KEY:0:20}..."
If any are empty, Supabase may not be running. Stop and tell the user:
"Local Supabase is not running. Run supabase start then retry /seed-club."
Step 2 — Tear down any previous seed
Run the following SQL to delete any existing seed data (identified by the slug qa-pine-ridge). This is idempotent — it's safe to run even if the seed doesn't exist yet.
psql "$DB_URL" <<'SQL'
DO $$
DECLARE
club_id_to_delete uuid;
BEGIN
SELECT id INTO club_id_to_delete FROM public.clubs WHERE slug = 'qa-pine-ridge';
IF club_id_to_delete IS NOT NULL THEN
-- Delete bookings and their players (cascade handles booking_players)
DELETE FROM public.bookings WHERE club_id = club_id_to_delete;
-- Delete blocks
DELETE FROM public.blocks WHERE club_id = club_id_to_delete;
-- Delete membership types
DELETE FROM public.membership_types WHERE club_id = club_id_to_delete;
-- Delete tee sheet settings
DELETE FROM public.tee_sheet_settings WHERE club_id = club_id_to_delete;
-- Delete club memberships
DELETE FROM public.club_memberships WHERE club_id = club_id_to_delete;
-- Delete the club
DELETE FROM public.clubs WHERE id = club_id_to_delete;
END IF;
END;
$$;
SQL
Then delete the seed auth users (auth.users cascade-deletes their profiles):
psql "$DB_URL" <<'SQL'
DELETE FROM auth.users
WHERE email IN (
'qa-admin@pine-ridge.example',
'qa-staff@pine-ridge.example',
'qa-member1@pine-ridge.example',
'qa-member2@pine-ridge.example',
'qa-member3@pine-ridge.example',
'qa-member4@pine-ridge.example'
);
SQL
Step 3 — Create auth users via Admin API
Create each user with a known password (Password123!) so QA can log in directly.
create_user() {
local EMAIL="$1"
local NAME="$2"
curl -sf -X POST "$API_URL/auth/v1/admin/users" \
-H "apikey: $SERVICE_KEY" \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"$EMAIL\",
\"password\": \"Password123!\",
\"email_confirm\": true,
\"user_metadata\": {\"display_name\": \"$NAME\"}
}" | jq -r '.id'
}
ADMIN_ID=$(create_user "qa-admin@pine-ridge.example" "Quinn Admin")
STAFF_ID=$(create_user "qa-staff@pine-ridge.example" "Sam Staff")
MEMBER1_ID=$(create_user "qa-member1@pine-ridge.example" "Alice Full")
MEMBER2_ID=$(create_user "qa-member2@pine-ridge.example" "Bob Full")
MEMBER3_ID=$(create_user "qa-member3@pine-ridge.example" "Carol Social")
MEMBER4_ID=$(create_user "qa-member4@pine-ridge.example" "Dave Social")
Verify all IDs were returned:
for VAR in ADMIN_ID STAFF_ID MEMBER1_ID MEMBER2_ID MEMBER3_ID MEMBER4_ID; do
echo "$VAR=${!VAR}"
if [ -z "${!VAR}" ] || [ "${!VAR}" = "null" ]; then
echo "ERROR: failed to create $VAR — check Supabase logs"
exit 1
fi
done
Step 4 — Seed club graph
The handle_new_user trigger auto-created profile rows when the auth users were inserted above. Now seed the rest of the graph.
psql "$DB_URL" <<SQL
DO \$\$
DECLARE
v_club_id uuid;
v_full_type_id uuid;
v_social_type_id uuid;
v_booking_id uuid;
BEGIN
-- Club
INSERT INTO public.clubs (name, slug, timezone, invite_code)
VALUES ('Pine Ridge Golf Club', 'qa-pine-ridge', 'America/Chicago', 'qa-invite-code-2026')
RETURNING id INTO v_club_id;
-- Memberships
INSERT INTO public.club_memberships (club_id, profile_id, role)
VALUES
(v_club_id, '$ADMIN_ID', 'club_admin'),
(v_club_id, '$STAFF_ID', 'staff'),
(v_club_id, '$MEMBER1_ID', 'member'),
(v_club_id, '$MEMBER2_ID', 'member'),
(v_club_id, '$MEMBER3_ID', 'member'),
(v_club_id, '$MEMBER4_ID', 'member');
-- Membership types
INSERT INTO public.membership_types (club_id, name, annual_round_limit, is_default)
VALUES (v_club_id, 'Full', NULL, true)
RETURNING id INTO v_full_type_id;
INSERT INTO public.membership_types (club_id, name, annual_round_limit, is_default)
VALUES (v_club_id, 'Social', 20, false)
RETURNING id INTO v_social_type_id;
-- Assign membership types
UPDATE public.club_memberships
SET membership_type_id = v_full_type_id
WHERE club_id = v_club_id AND profile_id IN ('$MEMBER1_ID', '$MEMBER2_ID');
UPDATE public.club_memberships
SET membership_type_id = v_social_type_id
WHERE club_id = v_club_id AND profile_id IN ('$MEMBER3_ID', '$MEMBER4_ID');
-- Tee sheet settings
INSERT INTO public.tee_sheet_settings (
club_id,
tee_time_interval_minutes,
first_tee_time,
last_tee_time,
operating_days,
booking_window_days,
booking_window_opening_time,
cancellation_deadline_hours,
cart_fleet_size,
estimated_round_duration_hours,
guest_annual_round_limit,
member_guest_invite_limit
) VALUES (
v_club_id,
10,
'07:00',
'17:00',
'{0,2,3,4,5,6}', -- closed Mondays (day 1)
7,
'06:00',
24,
12,
4.0,
6,
4
);
-- Sample booking: Member1 (Alice Full) + Guest with marketing consent
INSERT INTO public.bookings (club_id, tee_time_date, tee_time_start, member_id, notes, status)
VALUES (
v_club_id,
CURRENT_DATE + INTERVAL '2 days',
'09:00',
'$MEMBER1_ID',
'Birthday round — birthday cake in the cart',
'confirmed'
)
RETURNING id INTO v_booking_id;
INSERT INTO public.booking_players (booking_id, player_type, profile_id, display_name, transport)
VALUES (v_booking_id, 'member', '$MEMBER1_ID', 'Alice Full', 'riding');
INSERT INTO public.booking_players (booking_id, player_type, profile_id, display_name, transport, email, marketing_consent)
VALUES (v_booking_id, 'guest', NULL, 'Eve Guest', 'walking', 'eve.guest@example.com', true);
-- Sample booking: Member2 (Bob Full) solo walk
INSERT INTO public.bookings (club_id, tee_time_date, tee_time_start, member_id, notes, status)
VALUES (
v_club_id,
CURRENT_DATE + INTERVAL '3 days',
'08:10',
'$MEMBER2_ID',
'',
'confirmed'
)
RETURNING id INTO v_booking_id;
INSERT INTO public.booking_players (booking_id, player_type, profile_id, display_name, transport)
VALUES (v_booking_id, 'member', '$MEMBER2_ID', 'Bob Full', 'walking');
-- Block: recurring Wednesday league (4:00–6:00 PM for the next 3 months)
INSERT INTO public.blocks (
club_id, block_type, name,
start_date, end_date,
day_of_week, start_time, end_time,
is_recurring, created_by
) VALUES (
v_club_id, 'league', 'Wednesday Evening League',
CURRENT_DATE,
CURRENT_DATE + INTERVAL '3 months',
3, -- Wednesday
'16:00', '18:00',
true,
'$STAFF_ID'
);
END;
\$\$;
SQL
Step 5 — Verify
psql "$DB_URL" -c "
SELECT
c.name AS club,
COUNT(DISTINCT cm.id) AS memberships,
COUNT(DISTINCT mt.id) AS membership_types,
COUNT(DISTINCT b.id) AS bookings,
COUNT(DISTINCT bl.id) AS blocks
FROM public.clubs c
LEFT JOIN public.club_memberships cm ON cm.club_id = c.id
LEFT JOIN public.membership_types mt ON mt.club_id = c.id
LEFT JOIN public.bookings b ON b.club_id = c.id
LEFT JOIN public.blocks bl ON bl.club_id = c.id
WHERE c.slug = 'qa-pine-ridge'
GROUP BY c.name;
"
Expected output: 6 memberships, 2 membership types, 2 bookings, 1 block.
QA Login Credentials
All accounts use password Password123!.
| Role | Email | Notes |
|---|
| Club Admin | qa-admin@pine-ridge.example | Full admin access |
| Staff | qa-staff@pine-ridge.example | Can manage tee sheet + members |
| Member (Full) | qa-member1@pine-ridge.example | Alice Full — has a booking |
| Member (Full) | qa-member2@pine-ridge.example | Bob Full — has a booking |
| Member (Social) | qa-member3@pine-ridge.example | Carol Social — no bookings |
| Member (Social) | qa-member4@pine-ridge.example | Dave Social — no bookings |
Club invite link: http://localhost:5173/join/qa-pine-ridge?code=qa-invite-code-2026
Re-seeding
Simply re-run /seed-club. Step 2 tears everything down before Step 4 recreates it. All bookings, blocks, and memberships are reset to the fixture state.
Troubleshooting
- "relation auth.users does not exist": Supabase is not running. Run
supabase start.
- "duplicate key value violates unique constraint": Teardown may have been incomplete. Re-run
/seed-club — Step 2 will clean up.
- curl returns empty ID: Admin API may require the service-role key. Check
echo $SERVICE_KEY — if empty, re-run Step 1.
- Bookings fail with FK violation: The
handle_new_user trigger should have auto-created profile rows. Check with psql "$DB_URL" -c "SELECT email FROM public.profiles;" to verify profiles exist.