mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
fix: use dynamic URLs for cron jobs to prevent cross-env firing
Non-prod cron jobs were hardcoding production URLs, causing double notifications. Adds get_supabase_edge_function_url() helper that resolves from app_config.supabase_url per environment. Also adds text_answer question type to pack generation docs.
This commit is contained in:
1 parent
c8aaa8967d
commit
964fc25fc7
4 files changed
+247
-14
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
allowed-tools: Read, mcp__sauci-prod__execute_sql, mcp__sauci-prod__list_tables
|
||||
argument-hint: <pack-name> --category=<category> [--count=30] [--premium] [--types=swipe,who_likely,audio,photo]
|
||||
argument-hint: <pack-name> --category=<category> [--count=30] [--premium] [--types=swipe,who_likely,audio,photo,text_answer]
|
||||
description: Generate a question pack for Sauci following established content guidelines
|
||||
---
|
||||
|
||||
@@ -31,7 +31,7 @@ Extract from $ARGUMENTS:
|
||||
- **Category**: Required - which category this belongs to (or "new" to create)
|
||||
- **Question count**: Optional - default 30, can be 25-50
|
||||
- **Premium**: Optional flag - if pack should be premium
|
||||
- **Types**: Optional - comma-separated question types to generate. Default: `swipe,who_likely,audio,photo`. Options: `swipe`, `who_likely`, `audio`, `photo`
|
||||
- **Types**: Optional - comma-separated question types to generate. Default: `swipe,who_likely,audio,photo,text_answer`. Options: `swipe`, `who_likely`, `audio`, `photo`, `text_answer`
|
||||
|
||||
### Step 2: Understand Context
|
||||
|
||||
@@ -120,13 +120,26 @@ Each question MUST have a `question_type`. The default mix should include variet
|
||||
- "Take a selfie showing how you feel right now"
|
||||
- "Share a screenshot of your last text about your partner"
|
||||
|
||||
**`text_answer`** - Written response prompt
|
||||
- ALWAYS symmetric (partner_text = NULL, no inverse needed)
|
||||
- Partners type a written response
|
||||
- `config`: `{"max_length": 500}` (default 500)
|
||||
- Great for: reflections, love notes, confessions, creative writing, finish-the-sentence prompts
|
||||
- Writing can feel safer than speaking for vulnerable topics
|
||||
- Format: prompts that invite a thoughtful written response
|
||||
- Examples:
|
||||
- "Describe your partner in three words"
|
||||
- "Write a short love letter to your partner"
|
||||
- "Finish this sentence: When I'm with you I feel..."
|
||||
|
||||
#### 3.4 Type Distribution Guidelines
|
||||
|
||||
For a standard 30-question pack, aim for roughly:
|
||||
- **20-22 swipe** questions (the core experience)
|
||||
- **18-20 swipe** questions (the core experience)
|
||||
- **4-5 who_likely** questions
|
||||
- **2-3 audio** questions
|
||||
- **2-3 photo** questions
|
||||
- **2-3 text_answer** questions
|
||||
|
||||
Adjust based on what fits the pack's theme. Some packs may lean heavier on certain types.
|
||||
|
||||
@@ -147,7 +160,7 @@ Adjust based on what fits the pack's theme. Some packs may lean heavier on certa
|
||||
|
||||
#### 3.6 Critical: Inverse Questions and Database Linking (swipe questions only)
|
||||
|
||||
Non-swipe types (who_likely, audio, photo) are ALWAYS symmetric — no inverse needed.
|
||||
Non-swipe types (who_likely, audio, photo, text_answer) are ALWAYS symmetric — no inverse needed.
|
||||
|
||||
For EVERY asymmetric **swipe** question, create its inverse AND link them using the `inverse_of` column:
|
||||
|
||||
@@ -225,7 +238,7 @@ Before outputting, verify each question against this checklist:
|
||||
- [ ] who_likely questions use "Who is more likely to..." format
|
||||
- [ ] audio questions prompt a natural spoken response
|
||||
- [ ] photo questions prompt something visual and shareable
|
||||
- [ ] Non-swipe types have partner_text = NULL and no inverse
|
||||
- [ ] Non-swipe types (who_likely, audio, photo, text_answer) have partner_text = NULL and no inverse
|
||||
- [ ] Type distribution is balanced (see 3.4)
|
||||
|
||||
**Structure:**
|
||||
@@ -291,6 +304,11 @@ INSERT INTO questions (pack_id, text, partner_text, intensity, question_type, co
|
||||
-- photo questions (always symmetric, no inverse)
|
||||
INSERT INTO questions (pack_id, text, partner_text, intensity, question_type, config, inverse_of) VALUES
|
||||
('[pack_id]', 'Share a photo that makes you think of your partner', NULL, 1, 'photo', '{}', NULL);
|
||||
|
||||
-- text_answer questions (always symmetric, no inverse)
|
||||
INSERT INTO questions (pack_id, text, partner_text, intensity, question_type, config, inverse_of) VALUES
|
||||
('[pack_id]', 'Describe your partner in three words', NULL, 1, 'text_answer', '{"max_length": 500}', NULL),
|
||||
('[pack_id]', 'Write a short love letter to your partner', NULL, 1, 'text_answer', '{"max_length": 500}', NULL);
|
||||
```
|
||||
|
||||
Also provide a summary table:
|
||||
@@ -303,6 +321,7 @@ Also provide a summary table:
|
||||
| who_likely questions | X |
|
||||
| audio questions | X |
|
||||
| photo questions | X |
|
||||
| text_answer questions | X |
|
||||
| Symmetric (null partner_text) | X |
|
||||
| Asymmetric swipe pairs (with inverse_of links) | X pairs (X questions) |
|
||||
| With couple targeting | X |
|
||||
@@ -329,7 +348,7 @@ Also provide a summary table:
|
||||
- DON'T use cheesy or cliche language
|
||||
- DON'T assume heterosexual couples
|
||||
- DON'T skip the inverse question requirement for asymmetric swipe questions
|
||||
- DON'T create inverses for who_likely, audio, or photo questions (they're always symmetric)
|
||||
- DON'T create inverses for who_likely, audio, photo, or text_answer questions (they're always symmetric)
|
||||
- DON'T forget to set question_type - every question needs one
|
||||
- DON'T use interview-style questions ("Would you like to...")
|
||||
- DON'T treat inverse pairs as duplicates - they are REQUIRED (one's `text` = other's `partner_text`)
|
||||
@@ -204,6 +204,35 @@ This approach:
|
||||
- **Profile creation trigger**: The `on_auth_user_created` trigger automatically creates a profile when a user signs up. Never assume a profile exists without checking.
|
||||
- **Check for null profiles** in edge functions before performing operations.
|
||||
|
||||
### Cron Jobs (pg_cron) - IMPORTANT
|
||||
|
||||
**NEVER hardcode Supabase URLs in cron job migrations.** Migrations run on both prod and non-prod, so a hardcoded production URL will cause non-prod cron jobs to hit production (double-firing notifications, duplicate processing, etc.).
|
||||
|
||||
**Correct pattern** — use `get_supabase_edge_function_url()`:
|
||||
```sql
|
||||
SELECT cron.schedule(
|
||||
'my-cron-job',
|
||||
'*/5 * * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('my-edge-function')
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
This resolves the URL from `app_config.supabase_url` at migration-time, so each environment targets its own edge functions.
|
||||
|
||||
**If adding a new environment**, you must seed the URL before running migrations:
|
||||
```sql
|
||||
UPDATE app_config SET supabase_url = 'https://<project-ref>.supabase.co';
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
- Responses use UPSERT with `onConflict: "user_id,question_id"`
|
||||
- Match detection happens in `submit-response` edge function after each response
|
||||
|
||||
@@ -79,13 +79,27 @@ Partners take or share a photo. Great for selfies, throwbacks, creative challeng
|
||||
- "Share a screenshot of your last text about your partner"
|
||||
- "Show the view from where you are right now"
|
||||
|
||||
### `text_answer` - Written Response Prompt
|
||||
Partners type a written response. Great for reflections, love notes, confessions, creative writing, finish-the-sentence prompts.
|
||||
|
||||
- **ALWAYS symmetric** — partner_text = NULL, no inverse needed
|
||||
- `config`: `{"max_length": 500}`
|
||||
- Writing can feel safer than speaking for vulnerable topics
|
||||
- Format: prompts that invite a thoughtful written response
|
||||
- Examples:
|
||||
- "Describe your partner in three words"
|
||||
- "Write a short love letter to your partner"
|
||||
- "Finish this sentence: When I'm with you I feel..."
|
||||
- "Write down a fantasy you haven't said out loud"
|
||||
|
||||
### Type Distribution
|
||||
|
||||
For a standard 30-question pack, aim for roughly:
|
||||
- **20-22 swipe** questions (the core experience)
|
||||
- **18-20 swipe** questions (the core experience)
|
||||
- **4-5 who_likely** questions
|
||||
- **2-3 audio** questions
|
||||
- **2-3 photo** questions
|
||||
- **2-3 text_answer** questions
|
||||
|
||||
Adjust based on the pack's theme. A "Getting to Know You" pack might lean heavier on who_likely and audio. An explicit pack might be mostly swipe.
|
||||
|
||||
@@ -97,7 +111,7 @@ Each question has:
|
||||
- `text` - What Partner A sees
|
||||
- `partner_text` - What Partner B sees (NULL for symmetric questions and all non-swipe types)
|
||||
- `intensity` - 1-5 scale (for backend filtering, not shown in UI)
|
||||
- `question_type` - `swipe`, `who_likely`, `audio`, or `photo`
|
||||
- `question_type` - `swipe`, `who_likely`, `audio`, `photo`, or `text_answer`
|
||||
- `config` - JSONB for type-specific settings (e.g., `{"max_duration_seconds": 60}` for audio)
|
||||
- `allowed_couple_genders` - Which couple types see this (NULL = all)
|
||||
- `target_user_genders` - Which gender initiates (NULL = any)
|
||||
@@ -148,7 +162,7 @@ This ensures:
|
||||
|
||||
**IMPORTANT:**
|
||||
- Inverse pairs are NOT duplicates. When one question's `text` matches another's `partner_text`, they form a valid inverse pair. This is intentional and required - do not flag or remove these as duplicates.
|
||||
- Non-swipe types (who_likely, audio, photo) NEVER have inverses — they are always symmetric.
|
||||
- Non-swipe types (who_likely, audio, photo, text_answer) NEVER have inverses — they are always symmetric.
|
||||
|
||||
---
|
||||
|
||||
@@ -167,7 +181,7 @@ Controls which couple types see the question. Options: `male+male`, `female+male
|
||||
| Requires vagina | `['female+male', 'female+female']` | Exclude M+M |
|
||||
| Sex toys | NULL | Toys work for everyone |
|
||||
|
||||
**Note:** who_likely, audio, and photo questions are always NULL (they're gender-neutral by nature).
|
||||
**Note:** who_likely, audio, photo, and text_answer questions are always NULL (they're gender-neutral by nature).
|
||||
|
||||
### Initiator Targeting (target_user_genders)
|
||||
|
||||
@@ -278,7 +292,7 @@ Advanced/BDSM/extreme exploration
|
||||
- Use cheesy phrases ("make love", "intimate connection", "souls intertwining")
|
||||
- Assume heterosexual couples by default
|
||||
- Create one-sided asymmetric swipe questions (always make the inverse)
|
||||
- Create inverses for who_likely, audio, or photo (they're symmetric)
|
||||
- Create inverses for who_likely, audio, photo, or text_answer (they're symmetric)
|
||||
- Use time-specific language
|
||||
- Be preachy or educational in tone
|
||||
- Include judgment about activities
|
||||
@@ -309,7 +323,7 @@ When asked to create a pack:
|
||||
- What question types fit best for this theme?
|
||||
|
||||
3. **Generate questions**
|
||||
- Mix of swipe, who_likely, audio, and photo types
|
||||
- Mix of swipe, who_likely, audio, photo, and text_answer types
|
||||
- For swipe: mix of symmetric (null partner_text) and asymmetric questions
|
||||
- For EVERY asymmetric swipe question, immediately create its inverse
|
||||
- Vary sentence structure and openers
|
||||
@@ -349,7 +363,9 @@ INSERT INTO questions (id, pack_id, text, partner_text, intensity, question_type
|
||||
-- audio (always symmetric, no inverse)
|
||||
(gen_random_uuid(), '[pack_id]', 'Describe your perfect date in 30 seconds', NULL, 1, 'audio', '{"max_duration_seconds": 60}', NULL, NULL, NULL, NULL),
|
||||
-- photo (always symmetric, no inverse)
|
||||
(gen_random_uuid(), '[pack_id]', 'Share a photo that makes you smile', NULL, 1, 'photo', '{}', NULL, NULL, NULL, NULL);
|
||||
(gen_random_uuid(), '[pack_id]', 'Share a photo that makes you smile', NULL, 1, 'photo', '{}', NULL, NULL, NULL, NULL),
|
||||
-- text_answer (always symmetric, no inverse)
|
||||
(gen_random_uuid(), '[pack_id]', 'Describe your partner in three words', NULL, 1, 'text_answer', '{"max_length": 500}', NULL, NULL, NULL, NULL);
|
||||
```
|
||||
|
||||
---
|
||||
@@ -424,6 +440,15 @@ question_type: photo
|
||||
config: {}
|
||||
```
|
||||
|
||||
### text_answer
|
||||
```
|
||||
text: "Write a short love letter to your partner"
|
||||
partner_text: NULL
|
||||
intensity: 1
|
||||
question_type: text_answer
|
||||
config: {"max_length": 500}
|
||||
```
|
||||
|
||||
### Swipe - Steamy (Intensity 4) - With Couple Targeting
|
||||
```
|
||||
text: "Give your partner a blowjob in a risky location"
|
||||
@@ -470,7 +495,7 @@ config: {}
|
||||
- [ ] who_likely uses "Who is more likely to..." format
|
||||
- [ ] audio prompts invite a natural spoken response
|
||||
- [ ] photo prompts invite a visual response
|
||||
- [ ] Non-swipe types have partner_text = NULL and inverse_of = NULL
|
||||
- [ ] Non-swipe types (who_likely, audio, photo, text_answer) have partner_text = NULL and inverse_of = NULL
|
||||
- [ ] Type distribution is balanced
|
||||
|
||||
**Structure:**
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
-- Store the project's own Supabase URL in app_config so cron jobs can
|
||||
-- target the correct environment. This prevents non-prod cron jobs from
|
||||
-- accidentally hitting production edge functions.
|
||||
ALTER TABLE app_config
|
||||
ADD COLUMN IF NOT EXISTS supabase_url TEXT;
|
||||
|
||||
-- Helper function for building edge function URLs from app_config
|
||||
CREATE OR REPLACE FUNCTION get_supabase_edge_function_url(function_name TEXT)
|
||||
RETURNS TEXT
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT (SELECT supabase_url FROM app_config LIMIT 1)
|
||||
|| '/functions/v1/'
|
||||
|| function_name;
|
||||
$$;
|
||||
|
||||
-- Re-register all cron jobs using the helper function.
|
||||
-- format() resolves at migration-time, baking the URL into the cron command.
|
||||
-- IMPORTANT: app_config.supabase_url MUST be set before this migration runs.
|
||||
-- CI should run: UPDATE app_config SET supabase_url = '...' before db push,
|
||||
-- or it can be seeded manually per environment.
|
||||
|
||||
-- For safety, abort if supabase_url is not set
|
||||
DO $$
|
||||
BEGIN
|
||||
IF (SELECT supabase_url FROM app_config LIMIT 1) IS NULL THEN
|
||||
RAISE EXCEPTION 'app_config.supabase_url must be set before running this migration. '
|
||||
'Run: UPDATE app_config SET supabase_url = ''https://<project-ref>.supabase.co'';';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT cron.unschedule('send-pack-change-notifications');
|
||||
SELECT cron.schedule(
|
||||
'send-pack-change-notifications',
|
||||
'*/5 * * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-pack-change-notification')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('send-partner-activity-notifications');
|
||||
SELECT cron.schedule(
|
||||
'send-partner-activity-notifications',
|
||||
'*/5 * * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-partner-activity-notification')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('process-scheduled-releases');
|
||||
SELECT cron.schedule(
|
||||
'process-scheduled-releases',
|
||||
'*/5 * * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('process-scheduled-releases')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('check-streak-milestones');
|
||||
SELECT cron.schedule(
|
||||
'check-streak-milestones',
|
||||
'5 0 * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('check-streak-milestones')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('send-match-notification-digests');
|
||||
SELECT cron.schedule(
|
||||
'send-match-notification-digests',
|
||||
'* * * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-match-notification-digest')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('send-weekly-summary');
|
||||
SELECT cron.schedule(
|
||||
'send-weekly-summary',
|
||||
'0 10 * * 0',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-weekly-summary')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('send-unpaired-reminders');
|
||||
SELECT cron.schedule(
|
||||
'send-unpaired-reminders',
|
||||
'0 18 * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-unpaired-reminder')
|
||||
)
|
||||
);
|
||||
|
||||
SELECT cron.unschedule('send-catchup-reminders');
|
||||
SELECT cron.schedule(
|
||||
'send-catchup-reminders',
|
||||
'0 17 * * *',
|
||||
format(
|
||||
$cmd$
|
||||
SELECT net.http_post(
|
||||
url := %L,
|
||||
headers := jsonb_build_object('Content-Type', 'application/json'),
|
||||
body := '{}'::jsonb
|
||||
)
|
||||
$cmd$,
|
||||
get_supabase_edge_function_url('send-catchup-reminder')
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user