diff --git a/.github/workflows/migration-audit.yml b/.github/workflows/migration-audit.yml new file mode 100644 index 00000000..eff33ef1 --- /dev/null +++ b/.github/workflows/migration-audit.yml @@ -0,0 +1,22 @@ +name: Migration Audit + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + check-migration-sources: + name: Check single migration source of truth + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Make audit script executable + run: chmod +x scripts/check-no-backend-migrations.sh + + - name: Verify no active migrations in backend/migrations/ + run: ./scripts/check-no-backend-migrations.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43ac4232..1ff939bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -194,3 +194,43 @@ Thank you for helping make Synchro better! 🚀 ## Issue Delivery Notes When completing an issue, any long-form implementation artifacts, summaries, or delivery notes must be stored in the `docs/archive/` directory rather than the repository root. This keeps the root directory clean and ensures that active project entrypoints are easy to find. + +## Database Migrations + +### Single Source of Truth + +**All database schema changes must go through `supabase/migrations/` only.** + +The `backend/migrations/` directory existed previously but has been archived as part of Issue #655. +It must not be used for any new migrations. + +### How to Create a Migration + +1. Generate a new migration file: +```bash + supabase migration new your_migration_name +``` + This creates a timestamped file in `supabase/migrations/`. + +2. Write your SQL in that file. + +3. Apply it locally to test: +```bash + supabase db push +``` + +4. Commit the file and open a PR. + +### Rules + +- Never add `.sql` files directly to `backend/migrations/`. The CI check will fail. +- Never define the same table in both `supabase/migrations/` and anywhere else. +- All tables must have RLS enabled and at least one policy. +- Use `TIMESTAMPTZ` for all timestamp columns (not bare `TIMESTAMP`). +- Blockchain/Soroban timestamps are stored as `BIGINT` Unix epoch seconds - this is intentional. + +### Why supabase/migrations/ Wins + +SYNCRO uses Supabase as its database provider. The Supabase CLI is the authoritative migration +runner. Any migrations run outside of it will not be tracked in `supabase_migrations.schema_migrations` +and will cause sync errors. diff --git a/backend/migrations/019_add_quiet_hours_to_user_preferences.sql b/backend/migrations/_archived/019_add_quiet_hours_to_user_preferences.sql similarity index 100% rename from backend/migrations/019_add_quiet_hours_to_user_preferences.sql rename to backend/migrations/_archived/019_add_quiet_hours_to_user_preferences.sql diff --git a/backend/migrations/020_create_delayed_notifications.sql b/backend/migrations/_archived/020_create_delayed_notifications.sql similarity index 100% rename from backend/migrations/020_create_delayed_notifications.sql rename to backend/migrations/_archived/020_create_delayed_notifications.sql diff --git a/backend/migrations/021_create_telegram_connections.sql b/backend/migrations/_archived/021_create_telegram_connections.sql similarity index 100% rename from backend/migrations/021_create_telegram_connections.sql rename to backend/migrations/_archived/021_create_telegram_connections.sql diff --git a/backend/migrations/021_create_user_roles_table.sql b/backend/migrations/_archived/021_create_user_roles_table.sql similarity index 100% rename from backend/migrations/021_create_user_roles_table.sql rename to backend/migrations/_archived/021_create_user_roles_table.sql diff --git a/backend/migrations/022_create_csp_violations.sql b/backend/migrations/_archived/022_create_csp_violations.sql similarity index 100% rename from backend/migrations/022_create_csp_violations.sql rename to backend/migrations/_archived/022_create_csp_violations.sql diff --git a/backend/migrations/023_add_calendar_preferences.sql b/backend/migrations/_archived/023_add_calendar_preferences.sql similarity index 100% rename from backend/migrations/023_add_calendar_preferences.sql rename to backend/migrations/_archived/023_add_calendar_preferences.sql diff --git a/backend/migrations/023_add_currency_and_timezone_to_user_preferences.sql b/backend/migrations/_archived/023_add_currency_and_timezone_to_user_preferences.sql similarity index 100% rename from backend/migrations/023_add_currency_and_timezone_to_user_preferences.sql rename to backend/migrations/_archived/023_add_currency_and_timezone_to_user_preferences.sql diff --git a/backend/migrations/023_create_rescan_jobs.sql b/backend/migrations/_archived/023_create_rescan_jobs.sql similarity index 100% rename from backend/migrations/023_create_rescan_jobs.sql rename to backend/migrations/_archived/023_create_rescan_jobs.sql diff --git a/backend/migrations/20260325205500_add_unique_constraint_to_reminder_schedules.sql b/backend/migrations/_archived/20260325205500_add_unique_constraint_to_reminder_schedules.sql similarity index 100% rename from backend/migrations/20260325205500_add_unique_constraint_to_reminder_schedules.sql rename to backend/migrations/_archived/20260325205500_add_unique_constraint_to_reminder_schedules.sql diff --git a/backend/migrations/20260329114500_create_monitoring_rpc.sql b/backend/migrations/_archived/20260329114500_create_monitoring_rpc.sql similarity index 100% rename from backend/migrations/20260329114500_create_monitoring_rpc.sql rename to backend/migrations/_archived/20260329114500_create_monitoring_rpc.sql diff --git a/backend/migrations/20260329120000_add_api_key_auth_fields.sql b/backend/migrations/_archived/20260329120000_add_api_key_auth_fields.sql similarity index 100% rename from backend/migrations/20260329120000_add_api_key_auth_fields.sql rename to backend/migrations/_archived/20260329120000_add_api_key_auth_fields.sql diff --git a/backend/migrations/20260330100000_add_notifications_and_budgets.sql b/backend/migrations/_archived/20260330100000_add_notifications_and_budgets.sql similarity index 100% rename from backend/migrations/20260330100000_add_notifications_and_budgets.sql rename to backend/migrations/_archived/20260330100000_add_notifications_and_budgets.sql diff --git a/backend/migrations/20260426000000_blockchain_logs_tx_hash_unique.sql b/backend/migrations/_archived/20260426000000_blockchain_logs_tx_hash_unique.sql similarity index 100% rename from backend/migrations/20260426000000_blockchain_logs_tx_hash_unique.sql rename to backend/migrations/_archived/20260426000000_blockchain_logs_tx_hash_unique.sql diff --git a/backend/migrations/20260526000000_add_ops_metrics_indexes.sql b/backend/migrations/_archived/20260526000000_add_ops_metrics_indexes.sql similarity index 100% rename from backend/migrations/20260526000000_add_ops_metrics_indexes.sql rename to backend/migrations/_archived/20260526000000_add_ops_metrics_indexes.sql diff --git a/backend/migrations/20260527000000_add_dead_letter_handling.sql b/backend/migrations/_archived/20260527000000_add_dead_letter_handling.sql similarity index 100% rename from backend/migrations/20260527000000_add_dead_letter_handling.sql rename to backend/migrations/_archived/20260527000000_add_dead_letter_handling.sql diff --git a/backend/migrations/20260527000000_add_performance_indexes.sql b/backend/migrations/_archived/20260527000000_add_performance_indexes.sql similarity index 100% rename from backend/migrations/20260527000000_add_performance_indexes.sql rename to backend/migrations/_archived/20260527000000_add_performance_indexes.sql diff --git a/backend/migrations/_archived/README.md b/backend/migrations/_archived/README.md new file mode 100644 index 00000000..330ae5ba --- /dev/null +++ b/backend/migrations/_archived/README.md @@ -0,0 +1,9 @@ +# Archived Migrations + +These files were moved here as part of Issue #655. + +The canonical source of truth for all database migrations is: + supabase/migrations/ + +These files had overlapping concerns with supabase/migrations/ and have been +archived to prevent conflicts. Do not add new .sql files to this directory. diff --git a/backend/migrations/add_last_interaction_at.sql b/backend/migrations/_archived/add_last_interaction_at.sql similarity index 100% rename from backend/migrations/add_last_interaction_at.sql rename to backend/migrations/_archived/add_last_interaction_at.sql diff --git a/backend/migrations/add_pause_columns.sql b/backend/migrations/_archived/add_pause_columns.sql similarity index 100% rename from backend/migrations/add_pause_columns.sql rename to backend/migrations/_archived/add_pause_columns.sql diff --git a/backend/migrations/add_subscription_notes_and_custom_tags.sql b/backend/migrations/_archived/add_subscription_notes_and_custom_tags.sql similarity index 100% rename from backend/migrations/add_subscription_notes_and_custom_tags.sql rename to backend/migrations/_archived/add_subscription_notes_and_custom_tags.sql diff --git a/backend/migrations/add_subscription_notification_preferences.sql b/backend/migrations/_archived/add_subscription_notification_preferences.sql similarity index 100% rename from backend/migrations/add_subscription_notification_preferences.sql rename to backend/migrations/_archived/add_subscription_notification_preferences.sql diff --git a/backend/migrations/add_visibility_to_subscriptions.sql b/backend/migrations/_archived/add_visibility_to_subscriptions.sql similarity index 100% rename from backend/migrations/add_visibility_to_subscriptions.sql rename to backend/migrations/_archived/add_visibility_to_subscriptions.sql diff --git a/backend/migrations/create_audit_logs.sql b/backend/migrations/_archived/create_audit_logs.sql similarity index 100% rename from backend/migrations/create_audit_logs.sql rename to backend/migrations/_archived/create_audit_logs.sql diff --git a/backend/migrations/create_cancellation_guides.sql b/backend/migrations/_archived/create_cancellation_guides.sql similarity index 100% rename from backend/migrations/create_cancellation_guides.sql rename to backend/migrations/_archived/create_cancellation_guides.sql diff --git a/backend/migrations/create_email_scan_logs.sql b/backend/migrations/_archived/create_email_scan_logs.sql similarity index 100% rename from backend/migrations/create_email_scan_logs.sql rename to backend/migrations/_archived/create_email_scan_logs.sql diff --git a/backend/migrations/create_price_history.sql b/backend/migrations/_archived/create_price_history.sql similarity index 100% rename from backend/migrations/create_price_history.sql rename to backend/migrations/_archived/create_price_history.sql diff --git a/backend/migrations/create_renewal_tables.sql b/backend/migrations/_archived/create_renewal_tables.sql similarity index 100% rename from backend/migrations/create_renewal_tables.sql rename to backend/migrations/_archived/create_renewal_tables.sql diff --git a/backend/migrations/create_team_invitations.sql b/backend/migrations/_archived/create_team_invitations.sql similarity index 100% rename from backend/migrations/create_team_invitations.sql rename to backend/migrations/_archived/create_team_invitations.sql diff --git a/backend/migrations/create_webhooks.sql b/backend/migrations/_archived/create_webhooks.sql similarity index 100% rename from backend/migrations/create_webhooks.sql rename to backend/migrations/_archived/create_webhooks.sql diff --git a/backend/migrations/seed_cancellation_guides.sql b/backend/migrations/_archived/seed_cancellation_guides.sql similarity index 100% rename from backend/migrations/seed_cancellation_guides.sql rename to backend/migrations/_archived/seed_cancellation_guides.sql diff --git a/scripts/check-no-backend-migrations.sh b/scripts/check-no-backend-migrations.sh new file mode 100644 index 00000000..217e7324 --- /dev/null +++ b/scripts/check-no-backend-migrations.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# ============================================================= +# scripts/check-no-backend-migrations.sh +# +# CI guard: ensures no active SQL migration files exist in +# backend/migrations/. All migrations must live in supabase/migrations/. +# Part of Issue #655 fix - single source of truth for migrations. +# ============================================================= + +set -euo pipefail + +BACKEND_MIGRATIONS_DIR="backend/migrations" + +echo "Checking for active migrations in backend/migrations/ ..." + +ACTIVE_COUNT=$(find "${BACKEND_MIGRATIONS_DIR}" -maxdepth 1 -name "*.sql" 2>/dev/null | wc -l | tr -d ' ') + +if [ "${ACTIVE_COUNT}" -gt 0 ]; then + echo "" + echo "ERROR: Found ${ACTIVE_COUNT} active SQL file(s) in ${BACKEND_MIGRATIONS_DIR}/:" + find "${BACKEND_MIGRATIONS_DIR}" -maxdepth 1 -name "*.sql" + echo "" + echo "All database migrations must live in supabase/migrations/." + echo "Move any new migration files there and run: supabase db push" + exit 1 +fi + +echo "OK: No active migration files found in ${BACKEND_MIGRATIONS_DIR}/." +echo "All migrations are correctly owned by supabase/migrations/." +exit 0 diff --git a/supabase/migrations/20260531000000_reconcile_renewal_approvals.sql b/supabase/migrations/20260531000000_reconcile_renewal_approvals.sql new file mode 100644 index 00000000..cf20e7de --- /dev/null +++ b/supabase/migrations/20260531000000_reconcile_renewal_approvals.sql @@ -0,0 +1,53 @@ +-- ============================================================= +-- Migration: Reconcile renewal_approvals after deduplication audit +-- Issue #655 - Canonical migration source is supabase/migrations/ +-- ============================================================= + +-- Add subscription_id as a UUID foreign key (backend version had this) +ALTER TABLE public.renewal_approvals + ADD COLUMN IF NOT EXISTS subscription_id UUID REFERENCES public.subscriptions(id) ON DELETE CASCADE; + +-- Add a computed TIMESTAMPTZ column so app code can read expires_at as a proper timestamp +ALTER TABLE public.renewal_approvals + ADD COLUMN IF NOT EXISTS expires_at_ts TIMESTAMPTZ + GENERATED ALWAYS AS (to_timestamp(expires_at)) STORED; + +-- Add index on the new subscription_id column for fast lookups +CREATE INDEX IF NOT EXISTS idx_renewal_approvals_subscription_id + ON public.renewal_approvals (subscription_id); + +-- Document the column purposes clearly +COMMENT ON COLUMN public.renewal_approvals.blockchain_sub_id IS + 'The on-chain subscription ID from the Soroban contract (BIGINT).'; + +COMMENT ON COLUMN public.renewal_approvals.subscription_id IS + 'FK to public.subscriptions(id) — the app-level subscription this approval belongs to.'; + +COMMENT ON COLUMN public.renewal_approvals.expires_at IS + 'Unix epoch seconds (INTEGER) from the Soroban contract. Use expires_at_ts for display.'; + +COMMENT ON COLUMN public.renewal_approvals.expires_at_ts IS + 'Computed TIMESTAMPTZ version of expires_at for app-layer queries. Read-only.'; + +-- Enable RLS if not already enabled +ALTER TABLE public.renewal_approvals ENABLE ROW LEVEL SECURITY; + +-- Service role only policy +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE tablename = 'renewal_approvals' + AND policyname = 'Service role access only' + ) THEN + CREATE POLICY "Service role access only" + ON public.renewal_approvals + FOR ALL + USING (auth.role() = 'service_role') + WITH CHECK (auth.role() = 'service_role'); + END IF; +END $$; + +COMMENT ON TABLE public.renewal_approvals IS + 'Canonical renewal approvals table. Owned by supabase/migrations/. ' + 'The backend/migrations/ version has been archived (Issue #655).';