diff --git a/docs/ATOMIC_APPROVAL_TRANSACTIONS.md b/docs/ATOMIC_APPROVAL_TRANSACTIONS.md new file mode 100644 index 00000000..cd99ef51 --- /dev/null +++ b/docs/ATOMIC_APPROVAL_TRANSACTIONS.md @@ -0,0 +1,290 @@ +# Atomic Approval Transactions + +## Overview + +Phase 1 of the atomic transaction RPC implementation has been completed. This replaces the error-prone manual rollback logic in the `process-selective-approval` edge function with a true PostgreSQL ACID transaction. + +## Architecture + +### OLD Flow (process-selective-approval) +``` +Edge Function (2,759 lines) ──┐ + ├─ Create entity 1 ├─ Manual rollback on error + ├─ Create entity 2 ├─ Network failure = orphaned data + ├─ Create entity 3 ├─ Edge function crash = partial state + └─ Manual rollback if error─┘ +``` + +### NEW Flow (process-selective-approval-v2) +``` +Edge Function (~200 lines) + │ + └──> RPC: process_approval_transaction() + │ + └──> PostgreSQL Transaction ───────────┐ + ├─ Create entity 1 │ + ├─ Create entity 2 │ ATOMIC + ├─ Create entity 3 │ (all-or-nothing) + └─ Commit OR Rollback ──────────┘ + (any error = auto rollback) +``` + +## Key Benefits + +✅ **True ACID Transactions**: All operations succeed or fail together +✅ **Automatic Rollback**: ANY error triggers immediate rollback +✅ **Network Resilient**: Edge function crash = automatic rollback +✅ **Zero Orphaned Entities**: Impossible by design +✅ **Simpler Code**: Edge function reduced from 2,759 to ~200 lines + +## Database Functions Created + +### Main Transaction Function +```sql +process_approval_transaction( + p_submission_id UUID, + p_item_ids UUID[], + p_moderator_id UUID, + p_submitter_id UUID, + p_request_id TEXT DEFAULT NULL +) RETURNS JSONB +``` + +### Helper Functions +- `create_entity_from_submission()` - Creates entities (parks, rides, companies, etc.) +- `update_entity_from_submission()` - Updates existing entities +- `delete_entity_from_submission()` - Soft/hard deletes entities + +### Monitoring Table +- `approval_transaction_metrics` - Tracks performance, success rate, and rollbacks + +## Feature Flag + +The new flow is **disabled by default** to allow gradual rollout and testing. + +### Enabling the New Flow + +#### For Moderators (via Admin UI) +1. Navigate to Admin Settings +2. Find "Approval Transaction Mode" card +3. Toggle "Use Atomic Transaction RPC" to ON +4. Page will reload automatically + +#### Programmatically +```typescript +// Enable +localStorage.setItem('use_rpc_approval', 'true'); + +// Disable +localStorage.setItem('use_rpc_approval', 'false'); + +// Check status +const isEnabled = localStorage.getItem('use_rpc_approval') === 'true'; +``` + +## Testing Checklist + +### Basic Functionality ✓ +- [ ] Enable feature flag via admin UI +- [ ] Approve a simple submission (1-2 items) +- [ ] Verify entities created correctly +- [ ] Check console logs for "Using edge function: process-selective-approval-v2" +- [ ] Verify version history shows correct attribution + +### Error Scenarios ✓ +- [ ] Submit invalid data → verify full rollback +- [ ] Trigger validation error → verify no partial state +- [ ] Kill edge function mid-execution → verify auto rollback +- [ ] Check logs for "Transaction failed, rolling back" messages + +### Concurrent Operations ✓ +- [ ] Two moderators approve same submission → one succeeds, one gets locked error +- [ ] Verify only one set of entities created (no duplicates) + +### Data Integrity ✓ +- [ ] Run orphaned entity check (see SQL query below) +- [ ] Verify session variables cleared after transaction +- [ ] Check `approval_transaction_metrics` for success rate + +## Monitoring Queries + +### Check for Orphaned Entities +```sql +-- Should return 0 rows after migration +SELECT + 'parks' as table_name, + COUNT(*) as orphaned_count +FROM parks p +WHERE NOT EXISTS ( + SELECT 1 FROM park_versions pv + WHERE pv.park_id = p.id +) +AND p.created_at > NOW() - INTERVAL '24 hours' + +UNION ALL + +SELECT + 'rides' as table_name, + COUNT(*) as orphaned_count +FROM rides r +WHERE NOT EXISTS ( + SELECT 1 FROM ride_versions rv + WHERE rv.ride_id = r.id +) +AND r.created_at > NOW() - INTERVAL '24 hours'; +``` + +### Transaction Success Rate +```sql +SELECT + DATE_TRUNC('hour', created_at) as hour, + COUNT(*) as total_transactions, + COUNT(*) FILTER (WHERE success) as successful, + COUNT(*) FILTER (WHERE rollback_triggered) as rollbacks, + ROUND(AVG(duration_ms), 2) as avg_duration_ms, + ROUND(100.0 * COUNT(*) FILTER (WHERE success) / COUNT(*), 2) as success_rate +FROM approval_transaction_metrics +WHERE created_at > NOW() - INTERVAL '24 hours' +GROUP BY hour +ORDER BY hour DESC; +``` + +### Rollback Rate Alert +```sql +-- Alert if rollback_rate > 5% +SELECT + COUNT(*) FILTER (WHERE rollback_triggered) as rollbacks, + COUNT(*) as total_attempts, + ROUND(100.0 * COUNT(*) FILTER (WHERE rollback_triggered) / COUNT(*), 2) as rollback_rate +FROM approval_transaction_metrics +WHERE created_at > NOW() - INTERVAL '1 hour' +HAVING COUNT(*) FILTER (WHERE rollback_triggered) > 0; +``` + +## Rollback Plan + +If issues are detected after enabling the new flow: + +### Immediate Rollback (< 5 minutes) +```javascript +// Disable feature flag globally (or ask users to toggle off) +localStorage.setItem('use_rpc_approval', 'false'); +window.location.reload(); +``` + +### Data Recovery (if needed) +```sql +-- Identify submissions processed with v2 during problem window +SELECT + atm.submission_id, + atm.created_at, + atm.success, + atm.error_message +FROM approval_transaction_metrics atm +WHERE atm.created_at BETWEEN '2025-11-06 19:00:00' AND '2025-11-06 20:00:00' + AND atm.success = false + AND atm.rollback_triggered = true; + +-- Check for orphaned entities (if any exist) +-- Use the orphaned entity query above +``` + +## Success Metrics + +After full rollout, these metrics should be achieved: + +| Metric | Target | Current | +|--------|--------|---------| +| Zero orphaned entities | 0 | ✓ TBD | +| Zero manual rollback logs | 0 | ✓ TBD | +| Transaction success rate | >99% | ✓ TBD | +| Avg transaction time | <500ms | ✓ TBD | +| Rollback rate | <1% | ✓ TBD | + +## Deployment Phases + +### Phase 1: ✅ COMPLETE +- [x] Create RPC functions (helper + main transaction) +- [x] Create new edge function v2 +- [x] Add feature flag support to frontend +- [x] Create admin UI toggle +- [x] Add monitoring table + RLS policies + +### Phase 2: 🟡 IN PROGRESS +- [ ] Test with single moderator account +- [ ] Monitor metrics for 24 hours +- [ ] Verify zero orphaned entities +- [ ] Collect feedback from test moderator + +### Phase 3: 🔲 PENDING +- [ ] Enable for 10% of requests (weighted sampling) +- [ ] Monitor for 24 hours +- [ ] Check rollback rate < 1% + +### Phase 4: 🔲 PENDING +- [ ] Enable for 50% of requests +- [ ] Monitor for 48 hours +- [ ] Compare performance metrics with old flow + +### Phase 5: 🔲 PENDING +- [ ] Enable for 100% of requests +- [ ] Monitor for 1 week +- [ ] Mark old edge function as deprecated + +### Phase 6: 🔲 PENDING +- [ ] Remove old edge function +- [ ] Archive manual rollback code +- [ ] Update all documentation + +## Troubleshooting + +### Issue: Feature flag not working +**Symptom**: Logs still show "process-selective-approval" even with flag enabled +**Solution**: Clear localStorage and reload: `localStorage.clear(); window.location.reload()` + +### Issue: "RPC function not found" error +**Symptom**: Edge function fails with "process_approval_transaction not found" +**Solution**: Run the migration again or check function exists: +```sql +SELECT proname FROM pg_proc WHERE proname = 'process_approval_transaction'; +``` + +### Issue: High rollback rate (>5%) +**Symptom**: Many transactions rolling back in metrics +**Solution**: +1. Check error messages in `approval_transaction_metrics.error_message` +2. Disable feature flag immediately +3. Investigate root cause (validation issues, data integrity, etc.) + +### Issue: Orphaned entities detected +**Symptom**: Entities exist without corresponding versions +**Solution**: +1. Disable feature flag immediately +2. Run orphaned entity query to identify affected entities +3. Investigate cause (likely edge function crash during v1 flow) +4. Consider data cleanup (manual deletion or version creation) + +## FAQ + +**Q: Can I switch back to the old flow without data loss?** +A: Yes. Simply toggle off the feature flag. All data remains intact. + +**Q: What happens if the edge function crashes mid-transaction?** +A: PostgreSQL automatically rolls back the entire transaction. No orphaned data. + +**Q: How do I know which flow approved a submission?** +A: Check `approval_transaction_metrics` table. If a row exists, v2 was used. + +**Q: Can I use both flows simultaneously?** +A: Yes. The feature flag is per-browser, so different moderators can use different flows. + +**Q: When will the old flow be removed?** +A: After 30 days of stable operation at 100% rollout (Phase 6). + +## References + +- [Moderation Documentation](./versioning/MODERATION.md) +- [JSONB Elimination](./JSONB_ELIMINATION_COMPLETE.md) +- [Error Tracking](./ERROR_TRACKING.md) +- [PostgreSQL Transactions](https://www.postgresql.org/docs/current/tutorial-transactions.html) +- [ACID Properties](https://en.wikipedia.org/wiki/ACID) diff --git a/src/components/admin/ApprovalTransactionToggle.tsx b/src/components/admin/ApprovalTransactionToggle.tsx new file mode 100644 index 00000000..5c8c2e02 --- /dev/null +++ b/src/components/admin/ApprovalTransactionToggle.tsx @@ -0,0 +1,100 @@ +import { useState, useEffect } from 'react'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { AlertCircle, CheckCircle2, Database } from 'lucide-react'; +import { Alert, AlertDescription } from '@/components/ui/alert'; + +/** + * Admin toggle for switching between approval flows: + * - OLD: Manual rollback in edge function (error-prone) + * - NEW: Atomic PostgreSQL transaction (true ACID guarantees) + */ +export function ApprovalTransactionToggle() { + const [useRpcApproval, setUseRpcApproval] = useState(false); + + useEffect(() => { + // Read feature flag from localStorage + const enabled = localStorage.getItem('use_rpc_approval') === 'true'; + setUseRpcApproval(enabled); + }, []); + + const handleToggle = (checked: boolean) => { + localStorage.setItem('use_rpc_approval', checked ? 'true' : 'false'); + setUseRpcApproval(checked); + + // Force page reload to ensure all components pick up the new setting + window.location.reload(); + }; + + return ( + + + + + Approval Transaction Mode + + + Control which approval flow is used for moderation + + + +
+ + +
+ + {useRpcApproval ? ( + + + + Atomic Transaction Mode Enabled +
    +
  • ✅ True ACID transactions
  • +
  • ✅ Automatic rollback on errors
  • +
  • ✅ Network-resilient (edge function crash = auto rollback)
  • +
  • ✅ Zero orphaned entities
  • +
+
+
+ ) : ( + + + + Legacy Mode Active +
    +
  • ⚠️ Manual rollback logic (error-prone)
  • +
  • ⚠️ Risk of orphaned entities if edge function crashes
  • +
  • ⚠️ No true atomicity guarantee
  • +
+

+ Consider enabling Atomic Transaction Mode for improved reliability. +

+
+
+ )} + +
+

Testing Instructions:

+
    +
  1. Enable the toggle and approve a submission
  2. +
  3. Check logs for "Using edge function: process-selective-approval-v2"
  4. +
  5. Verify no orphaned entities in the database
  6. +
  7. Test error scenarios (invalid data, network issues)
  8. +
+
+
+
+ ); +} diff --git a/src/lib/moderation/actions.ts b/src/lib/moderation/actions.ts index 24f663c2..fee9dcb4 100644 --- a/src/lib/moderation/actions.ts +++ b/src/lib/moderation/actions.ts @@ -177,14 +177,42 @@ export async function approvePhotoSubmission( * @param itemIds - Array of item IDs to approve * @returns Action result */ +/** + * Feature flag to enable atomic transaction RPC approval flow. + * Set to true to use the new process-selective-approval-v2 edge function + * that wraps the entire approval in a single PostgreSQL transaction. + * + * Benefits of v2: + * - True atomic transactions (all-or-nothing) + * - Automatic rollback on ANY error + * - Network-resilient (edge function crash = auto rollback) + * - Eliminates orphaned entities + * + * To enable: localStorage.setItem('use_rpc_approval', 'true') + * To disable: localStorage.setItem('use_rpc_approval', 'false') + */ +const USE_RPC_APPROVAL = typeof window !== 'undefined' && + localStorage.getItem('use_rpc_approval') === 'true'; + export async function approveSubmissionItems( supabase: SupabaseClient, submissionId: string, itemIds: string[] ): Promise { try { + // Use v2 edge function if feature flag is enabled + const edgeFunctionName = USE_RPC_APPROVAL + ? 'process-selective-approval-v2' + : 'process-selective-approval'; + + console.log(`[Approval] Using edge function: ${edgeFunctionName}`, { + submissionId, + itemCount: itemIds.length, + useRpcApproval: USE_RPC_APPROVAL + }); + const { data: approvalData, error: approvalError, requestId } = await invokeWithTracking( - 'process-selective-approval', + edgeFunctionName, { itemIds, submissionId, diff --git a/supabase/config.toml b/supabase/config.toml index ad9deb02..23a39caf 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -42,6 +42,9 @@ verify_jwt = true [functions.process-selective-approval] verify_jwt = false +[functions.process-selective-approval-v2] +verify_jwt = false + [functions.send-escalation-notification] verify_jwt = true diff --git a/supabase/migrations/20251106201229_5e92d25c-2e3c-4700-a72a-5136867e6cfd.sql b/supabase/migrations/20251106201229_5e92d25c-2e3c-4700-a72a-5136867e6cfd.sql new file mode 100644 index 00000000..38135d85 --- /dev/null +++ b/supabase/migrations/20251106201229_5e92d25c-2e3c-4700-a72a-5136867e6cfd.sql @@ -0,0 +1,28 @@ +-- Enable RLS on approval_transaction_metrics table +ALTER TABLE approval_transaction_metrics ENABLE ROW LEVEL SECURITY; + +-- Policy: Only moderators and admins can view metrics +CREATE POLICY "Moderators can view approval metrics" +ON approval_transaction_metrics +FOR SELECT +TO authenticated +USING ( + EXISTS ( + SELECT 1 FROM user_roles + WHERE user_roles.user_id = auth.uid() + AND user_roles.role IN ('moderator', 'admin', 'superuser') + ) +); + +-- Policy: System can insert metrics (SECURITY DEFINER functions) +CREATE POLICY "System can insert approval metrics" +ON approval_transaction_metrics +FOR INSERT +TO authenticated +WITH CHECK (true); + +COMMENT ON POLICY "Moderators can view approval metrics" ON approval_transaction_metrics IS +'Allows moderators, admins, and superusers to view approval transaction metrics for monitoring and analytics'; + +COMMENT ON POLICY "System can insert approval metrics" ON approval_transaction_metrics IS +'Allows the process_approval_transaction function to log metrics. The function is SECURITY DEFINER so it runs with elevated privileges'; \ No newline at end of file