Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/migration-audit.yml
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
9 changes: 9 additions & 0 deletions backend/migrations/_archived/README.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions scripts/check-no-backend-migrations.sh
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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).';
Loading