diff --git a/.github/workflows/migration-sequence.yml b/.github/workflows/migration-sequence.yml new file mode 100644 index 00000000..d39de0b5 --- /dev/null +++ b/.github/workflows/migration-sequence.yml @@ -0,0 +1,31 @@ +name: Migration sequence + +on: + pull_request: + paths: + - 'supabase/migrations/**' + - 'supabase/config.toml' + - 'supabase/tests/migrations/**' + - '.github/workflows/migration-sequence.yml' + +permissions: + contents: read + +jobs: + migration-sequence: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: supabase/setup-cli@v1 + with: + version: latest + - name: Start local Supabase + run: supabase start + - name: Apply every migration to a fresh database + run: supabase db reset --local + - name: Verify schema baseline + run: | + sudo apt-get update + sudo apt-get install --yes postgresql-client + eval "$(supabase status -o env)" + psql "$DB_URL" -v ON_ERROR_STOP=1 -f supabase/tests/migrations/schema-smoke.sql \ No newline at end of file diff --git a/docs/cross-region-auth-integration-guide.md b/docs/cross-region-auth-integration-guide.md index b825d358..9869e666 100644 --- a/docs/cross-region-auth-integration-guide.md +++ b/docs/cross-region-auth-integration-guide.md @@ -182,6 +182,17 @@ const makeAuthRequest = async (email: string, password: string) => { ## Error Handling +### Reconcile Pending Sign-Ups + +If a sign-up profile could not be synchronized to every region, its auth audit record is marked with `syncStatus: "pending"`. Configure a high-entropy `REGIONAL_AUTH_RECONCILIATION_TOKEN` secret for the reconciliation function, then trigger it manually with: + +```sh +curl --fail-with-body -X POST "$SUPABASE_URL/functions/v1/regional-auth-reconcile" \ + -H "Authorization: Bearer $REGIONAL_AUTH_RECONCILIATION_TOKEN" +``` + +The function retries up to 100 pending records per region per request and reports the scanned, repaired, and still-pending counts. Repeat the request while `pending` is nonzero; records that remain incomplete stay marked for retry. + ### Handle Region Failover The system automatically fails over, but you should handle the response metadata: diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2623529e..06bab0ad 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -8,7 +8,7 @@ "build": "tsc", "dev": "tsc --watch", "lint": "eslint src/**/*.ts", - "test": "vitest run" + "test": "vitest run --typecheck" }, "devDependencies": { "typescript": "^5.3.3", diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 79381e5b..456ad418 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -7,6 +7,8 @@ export type { DeploymentStatusType } from '@craft/types'; +export type SubscriptionTier = 'free' | 'pro' | 'enterprise'; + /** * Configuration options for CraftClient. * @property baseUrl - The base URL of the CRAFT API (e.g., https://craft.app) diff --git a/packages/sdk/src/type-parity.test-d.ts b/packages/sdk/src/type-parity.test-d.ts new file mode 100644 index 00000000..77d3e574 --- /dev/null +++ b/packages/sdk/src/type-parity.test-d.ts @@ -0,0 +1,32 @@ +import { expectTypeOf } from 'vitest'; +import type { + DeploymentStatusType as SharedDeploymentStatusType, + SubscriptionTier as SharedSubscriptionTier, + Template as SharedTemplate, + User as SharedUser, +} from '../../types/src'; +import type { + DeploymentStatusType, + SubscriptionTier, + Template, + UserProfile, +} from './client'; + +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); + +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); + +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); +expectTypeOf().toEqualTypeOf(); + +export const intentionalTypeDivergences = { + UserProfile: 'The SDK API profile includes fullName, which shared User omits.', + Template: 'The SDK API response omits shared template persistence and customization fields.', +} as const; \ No newline at end of file diff --git a/packages/sdk/tsconfig.typecheck.json b/packages/sdk/tsconfig.typecheck.json new file mode 100644 index 00000000..32df6ea7 --- /dev/null +++ b/packages/sdk/tsconfig.typecheck.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "incremental": false, + "noEmit": true, + "rootDir": ".." + }, + "include": ["src/**/*.test-d.ts"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index 8e730d50..4639dfff 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -5,4 +5,7 @@ export default defineConfig({ globals: true, environment: 'node', }, + typecheck: { + tsconfig: './tsconfig.typecheck.json', + }, }); diff --git a/supabase/config.toml b/supabase/config.toml index 512e4984..76e8de01 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -54,6 +54,10 @@ verify_jwt = false key = "regional-auth-token-refresh" verify_jwt = false +[functions."regional-auth-reconcile"] +key = "regional-auth-reconcile" +verify_jwt = false + # Regional Health Check [functions."regional-health-check"] key = "regional-health-check" diff --git a/supabase/functions/regional-auth-reconcile/index.ts b/supabase/functions/regional-auth-reconcile/index.ts new file mode 100644 index 00000000..6ed62481 --- /dev/null +++ b/supabase/functions/regional-auth-reconcile/index.ts @@ -0,0 +1,75 @@ +import { serve } from 'https://deno.land/std@0.208.0/http/server.ts'; +import { SUPPORTED_REGIONS } from '../_shared/regions.ts'; +import { getRegionalSupabaseAdmin } from '../regional-auth/auth-utils.ts'; +import { repairUserStateConsistency } from '../regional-auth/consistency-validators.ts'; + +serve(async (req: Request) => { + if (req.method !== 'POST') { + return new Response('Method not allowed', { status: 405 }); + } + + const reconciliationToken = Deno.env.get('REGIONAL_AUTH_RECONCILIATION_TOKEN'); + if (!reconciliationToken || req.headers.get('authorization') !== `Bearer ${reconciliationToken}`) { + return new Response('Unauthorized', { status: 401 }); + } + + let scanned = 0; + let repaired = 0; + let pending = 0; + const failedRegions: string[] = []; + + for (const region of SUPPORTED_REGIONS) { + const admin = getRegionalSupabaseAdmin(region); + const { data: records, error } = await admin + .from('auth_audit_logs') + .select('id, user_id, region, details') + .eq('event_type', 'failure') + .contains('details', { syncStatus: 'pending' }) + .not('user_id', 'is', null) + .limit(100); + + if (error) { + failedRegions.push(region); + console.error(`Failed to load pending signup repairs in ${region}:`, error.message); + continue; + } + + for (const record of records ?? []) { + scanned++; + let isRepaired = false; + + try { + const result = await repairUserStateConsistency(record.user_id, record.region); + isRepaired = result.repaired; + } catch (repairError) { + console.error(`Pending signup repair failed for ${record.user_id}:`, repairError); + } + + const details = { + ...((record.details ?? {}) as Record), + syncStatus: isRepaired ? 'complete' : 'pending', + needsRepair: !isRepaired, + repairAttemptedAt: new Date().toISOString(), + }; + const { error: updateError } = await admin + .from('auth_audit_logs') + .update({ details }) + .eq('id', record.id); + + if (updateError) { + failedRegions.push(region); + pending++; + console.error(`Failed to update signup repair record ${record.id}:`, updateError.message); + } else if (isRepaired) { + repaired++; + } else { + pending++; + } + } + } + + return new Response(JSON.stringify({ scanned, repaired, pending, failedRegions }), { + status: failedRegions.length ? 503 : 200, + headers: { 'Content-Type': 'application/json' }, + }); +}); \ No newline at end of file diff --git a/supabase/functions/regional-auth/partial-sync-recovery.test.ts b/supabase/functions/regional-auth/partial-sync-recovery.test.ts new file mode 100644 index 00000000..d21ebfdc --- /dev/null +++ b/supabase/functions/regional-auth/partial-sync-recovery.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from 'vitest'; +import { recoverPartialSignupSync } from './partial-sync-recovery.ts'; + +describe('partial signup sync recovery', () => { + it('records a pending repair and attempts immediate recovery', async () => { + const recordRetryableFailure = vi.fn().mockResolvedValue(undefined); + const repair = vi.fn().mockRejectedValue(new Error('secondary region unavailable')); + const onRepairFailure = vi.fn(); + + await recoverPartialSignupSync( + { + userId: 'user-1', + region: 'us-east-1', + requestId: 'request-1', + errors: { 'eu-west-1': 'connection timed out' }, + }, + { recordRetryableFailure, repair, onRepairFailure }, + ); + + expect(recordRetryableFailure).toHaveBeenCalledWith( + 'request-1-sync-failure', + expect.objectContaining({ + failedRegions: ['eu-west-1'], + syncStatus: 'pending', + needsRepair: true, + }), + ); + expect(repair).toHaveBeenCalledOnce(); + expect(onRepairFailure).toHaveBeenCalledWith(expect.any(Error)); + }); +}); \ No newline at end of file diff --git a/supabase/functions/regional-auth/partial-sync-recovery.ts b/supabase/functions/regional-auth/partial-sync-recovery.ts new file mode 100644 index 00000000..e2cc4c7b --- /dev/null +++ b/supabase/functions/regional-auth/partial-sync-recovery.ts @@ -0,0 +1,29 @@ +export interface PartialSignupSyncFailure { + requestId: string; + errors: Record; +} + +export interface PartialSignupSyncRecoveryDependencies { + recordRetryableFailure(requestId: string, details: Record): Promise; + repair(): Promise; + onRepairFailure(error: unknown): void; +} + +export async function recoverPartialSignupSync( + failure: PartialSignupSyncFailure, + dependencies: PartialSignupSyncRecoveryDependencies, +): Promise { + await dependencies.recordRetryableFailure(`${failure.requestId}-sync-failure`, { + reason: 'cross-region profile sync incomplete', + failedRegions: Object.keys(failure.errors), + errors: failure.errors, + syncStatus: 'pending', + needsRepair: true, + }); + + try { + await dependencies.repair(); + } catch (error) { + dependencies.onRepairFailure(error); + } +} \ No newline at end of file diff --git a/supabase/functions/regional-auth/sign-up.ts b/supabase/functions/regional-auth/sign-up.ts index cee187e8..94cf3eac 100644 --- a/supabase/functions/regional-auth/sign-up.ts +++ b/supabase/functions/regional-auth/sign-up.ts @@ -18,6 +18,7 @@ import { type AuthResponse, } from './auth-utils.ts'; import { repairUserStateConsistency } from './consistency-validators.ts'; +import { recoverPartialSignupSync } from './partial-sync-recovery.ts'; interface SignUpRequest { email: string; @@ -183,29 +184,16 @@ async function handleSignUp(req: Request): Promise { ); if (!syncResult.synced) { - // #977 fix: a failed cross-region profile sync must not be silently - // dropped. We: - // 1. Write a durable audit record so operators can query partial-sync - // failures and the follow-up repair job has a discoverable trace. - // 2. Attempt an inline repair via repairUserStateConsistency() so the - // state is corrected as soon as possible without blocking the caller. - // The 201 response contract is unchanged — sign-up succeeds from the - // user's perspective regardless of cross-region replication status. - - // Step 1: durable audit log with needsRepair flag - await logAuthEvent(userId, 'failure', region, `${requestId}-sync-failure`, { - reason: 'cross-region profile sync incomplete', - failedRegions: Object.keys(syncResult.errors), - errors: syncResult.errors, - needsRepair: true, - }); - - // Step 2: attempt inline repair (best-effort; errors are logged, not thrown) - try { - await repairUserStateConsistency(userId, region); - } catch (repairError) { - console.error(`Inline repair failed for user ${userId}:`, repairError); - } + await recoverPartialSignupSync( + { requestId, errors: syncResult.errors }, + { + recordRetryableFailure: (syncFailureRequestId, details) => + logAuthEvent(userId, 'failure', region, syncFailureRequestId, details), + repair: () => repairUserStateConsistency(userId, region).then(() => undefined), + onRepairFailure: (repairError) => + console.error(`Inline repair failed for user ${userId}:`, repairError), + }, + ); } // Log successful signup (includes sync outcome so operators have full picture) diff --git a/supabase/functions/regional-router/index.ts b/supabase/functions/regional-router/index.ts index 82576863..b6b25871 100644 --- a/supabase/functions/regional-router/index.ts +++ b/supabase/functions/regional-router/index.ts @@ -10,12 +10,7 @@ import { serve } from 'https://deno.land/std@0.208.0/http/server.ts'; import { SUPPORTED_REGIONS, getRegionalEndpointConfig, detectRegionFromRequest } from '../_shared/regions.ts'; - -interface RegionEndpoint { - region: string; - baseUrl: string; - priority: number; -} +import { NoHealthyRegionsError, orderHealthyEndpoints, type RegionEndpoint } from './routing-policy.ts'; interface RoutingDecision { targetRegion: string; @@ -86,22 +81,7 @@ async function makeRoutingDecision( // Get health status of regions const healthStatus = await getRegionHealthStatus(); - // Sort endpoints by health and then by whether they match detected region - const sortedEndpoints = [...endpoints].sort((a, b) => { - const aHealthy = healthStatus.get(a.region) ?? false; - const bHealthy = healthStatus.get(b.region) ?? false; - - // Prioritize healthy regions - if (aHealthy !== bHealthy) { - return aHealthy ? -1 : 1; - } - - // Among healthy/unhealthy, prioritize detected region - const aMatches = a.region === detectedRegion ? 1 : 0; - const bMatches = b.region === detectedRegion ? 1 : 0; - - return bMatches - aMatches; - }); + const sortedEndpoints = orderHealthyEndpoints(endpoints, healthStatus, detectedRegion); const selectedEndpoint = sortedEndpoints[0]; const reason = @@ -192,7 +172,10 @@ async function handleRouting(req: Request): Promise { error: 'Routing failed', details: String(error), }), - { status: 502, headers: { 'Content-Type': 'application/json' } } + { + status: error instanceof NoHealthyRegionsError ? 503 : 502, + headers: { 'Content-Type': 'application/json' }, + } ); } } @@ -217,7 +200,10 @@ async function handleRoutingInfo(req: Request): Promise { error: 'Failed to get routing information', details: String(error), }), - { status: 500, headers: { 'Content-Type': 'application/json' } } + { + status: error instanceof NoHealthyRegionsError ? 503 : 500, + headers: { 'Content-Type': 'application/json' }, + } ); } } diff --git a/supabase/functions/regional-router/routing-policy.test.ts b/supabase/functions/regional-router/routing-policy.test.ts new file mode 100644 index 00000000..22c72034 --- /dev/null +++ b/supabase/functions/regional-router/routing-policy.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { + NoHealthyRegionsError, + orderHealthyEndpoints, + type RegionEndpoint, +} from './routing-policy.ts'; + +const endpoints: RegionEndpoint[] = [ + { region: 'us-east-1', baseUrl: 'https://us.example.com', priority: 1 }, + { region: 'eu-west-1', baseUrl: 'https://eu.example.com', priority: 1 }, +]; + +describe('regional routing policy', () => { + it('fails with an actionable error when every region is unhealthy', () => { + expect(() => + orderHealthyEndpoints( + endpoints, + new Map([ + ['us-east-1', false], + ['eu-west-1', false], + ]), + 'us-east-1', + ), + ).toThrow(NoHealthyRegionsError); + }); + + it('prefers the detected region among healthy regions', () => { + expect( + orderHealthyEndpoints( + endpoints, + new Map([ + ['us-east-1', true], + ['eu-west-1', true], + ]), + 'eu-west-1', + )[0].region, + ).toBe('eu-west-1'); + }); +}); \ No newline at end of file diff --git a/supabase/functions/regional-router/routing-policy.ts b/supabase/functions/regional-router/routing-policy.ts new file mode 100644 index 00000000..c44c562f --- /dev/null +++ b/supabase/functions/regional-router/routing-policy.ts @@ -0,0 +1,32 @@ +export interface RegionEndpoint { + region: string; + baseUrl: string; + priority: number; +} + +export class NoHealthyRegionsError extends Error { + constructor() { + super('No healthy regions are available; retry after regional health recovers'); + this.name = 'NoHealthyRegionsError'; + } +} + +export function orderHealthyEndpoints( + endpoints: RegionEndpoint[], + healthStatus: ReadonlyMap, + detectedRegion: string, +): RegionEndpoint[] { + if (!endpoints.some((endpoint) => healthStatus.get(endpoint.region) === true)) { + throw new NoHealthyRegionsError(); + } + + return [...endpoints].sort((a, b) => { + const aHealthy = healthStatus.get(a.region) ?? false; + const bHealthy = healthStatus.get(b.region) ?? false; + if (aHealthy !== bHealthy) return aHealthy ? -1 : 1; + + const aMatches = a.region === detectedRegion ? 1 : 0; + const bMatches = b.region === detectedRegion ? 1 : 0; + return bMatches - aMatches; + }); +} \ No newline at end of file diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/20260926000001_initial_schema.sql similarity index 100% rename from supabase/migrations/001_initial_schema.sql rename to supabase/migrations/20260926000001_initial_schema.sql diff --git a/supabase/migrations/002_row_level_security.sql b/supabase/migrations/20260926000002_row_level_security.sql similarity index 100% rename from supabase/migrations/002_row_level_security.sql rename to supabase/migrations/20260926000002_row_level_security.sql diff --git a/supabase/migrations/003_seed_templates.sql b/supabase/migrations/20260926000003_seed_templates.sql similarity index 100% rename from supabase/migrations/003_seed_templates.sql rename to supabase/migrations/20260926000003_seed_templates.sql diff --git a/supabase/migrations/004_github_token_metadata.sql b/supabase/migrations/20260926000004_github_token_metadata.sql similarity index 100% rename from supabase/migrations/004_github_token_metadata.sql rename to supabase/migrations/20260926000004_github_token_metadata.sql diff --git a/supabase/migrations/005_create_deployment_logs.sql b/supabase/migrations/20260926000005_create_deployment_logs.sql similarity index 100% rename from supabase/migrations/005_create_deployment_logs.sql rename to supabase/migrations/20260926000005_create_deployment_logs.sql diff --git a/supabase/migrations/006_payment_idempotency_keys.sql b/supabase/migrations/20260926000006_payment_idempotency_keys.sql similarity index 100% rename from supabase/migrations/006_payment_idempotency_keys.sql rename to supabase/migrations/20260926000006_payment_idempotency_keys.sql diff --git a/supabase/migrations/006_secure_token_storage.sql b/supabase/migrations/20260926000007_secure_token_storage.sql similarity index 100% rename from supabase/migrations/006_secure_token_storage.sql rename to supabase/migrations/20260926000007_secure_token_storage.sql diff --git a/supabase/migrations/007_field_encryption_stripe.sql b/supabase/migrations/20260926000008_field_encryption_stripe.sql similarity index 100% rename from supabase/migrations/007_field_encryption_stripe.sql rename to supabase/migrations/20260926000008_field_encryption_stripe.sql diff --git a/supabase/migrations/008_github_vercel_deployments.sql b/supabase/migrations/20260926000009_github_vercel_deployments.sql similarity index 100% rename from supabase/migrations/008_github_vercel_deployments.sql rename to supabase/migrations/20260926000009_github_vercel_deployments.sql diff --git a/supabase/migrations/009_deployment_updates_rollout.sql b/supabase/migrations/20260926000010_deployment_updates_rollout.sql similarity index 100% rename from supabase/migrations/009_deployment_updates_rollout.sql rename to supabase/migrations/20260926000010_deployment_updates_rollout.sql diff --git a/supabase/migrations/010_auth_audit_logs_cross_region.sql b/supabase/migrations/20260926000011_auth_audit_logs_cross_region.sql similarity index 100% rename from supabase/migrations/010_auth_audit_logs_cross_region.sql rename to supabase/migrations/20260926000011_auth_audit_logs_cross_region.sql diff --git a/supabase/migrations/010_deployment_soft_delete.sql b/supabase/migrations/20260926000012_deployment_soft_delete.sql similarity index 100% rename from supabase/migrations/010_deployment_soft_delete.sql rename to supabase/migrations/20260926000012_deployment_soft_delete.sql diff --git a/supabase/migrations/010_github_app_installations.sql b/supabase/migrations/20260926000013_github_app_installations.sql similarity index 100% rename from supabase/migrations/010_github_app_installations.sql rename to supabase/migrations/20260926000013_github_app_installations.sql diff --git a/supabase/migrations/010_vercel_blue_green_aliases.sql b/supabase/migrations/20260926000014_vercel_blue_green_aliases.sql similarity index 100% rename from supabase/migrations/010_vercel_blue_green_aliases.sql rename to supabase/migrations/20260926000014_vercel_blue_green_aliases.sql diff --git a/supabase/migrations/011_analytics_query_optimization.sql b/supabase/migrations/20260926000015_analytics_query_optimization.sql similarity index 100% rename from supabase/migrations/011_analytics_query_optimization.sql rename to supabase/migrations/20260926000015_analytics_query_optimization.sql diff --git a/supabase/migrations/011_branding_asset_storage_policy.sql b/supabase/migrations/20260926000016_branding_asset_storage_policy.sql similarity index 100% rename from supabase/migrations/011_branding_asset_storage_policy.sql rename to supabase/migrations/20260926000016_branding_asset_storage_policy.sql diff --git a/supabase/migrations/011_metered_billing_usage_tracking.sql b/supabase/migrations/20260926000017_metered_billing_usage_tracking.sql similarity index 100% rename from supabase/migrations/011_metered_billing_usage_tracking.sql rename to supabase/migrations/20260926000017_metered_billing_usage_tracking.sql diff --git a/supabase/migrations/012_multi_provider_oauth.sql b/supabase/migrations/20260926000018_multi_provider_oauth.sql similarity index 100% rename from supabase/migrations/012_multi_provider_oauth.sql rename to supabase/migrations/20260926000018_multi_provider_oauth.sql diff --git a/supabase/migrations/012_pgcron_automated_cleanup.sql b/supabase/migrations/20260926000019_pgcron_automated_cleanup.sql similarity index 100% rename from supabase/migrations/012_pgcron_automated_cleanup.sql rename to supabase/migrations/20260926000019_pgcron_automated_cleanup.sql diff --git a/supabase/migrations/013_github_webhook_delivery_tracking.sql b/supabase/migrations/20260926000020_github_webhook_delivery_tracking.sql similarity index 100% rename from supabase/migrations/013_github_webhook_delivery_tracking.sql rename to supabase/migrations/20260926000020_github_webhook_delivery_tracking.sql diff --git a/supabase/migrations/014_artifact_storage_namespace.sql b/supabase/migrations/20260926000021_artifact_storage_namespace.sql similarity index 100% rename from supabase/migrations/014_artifact_storage_namespace.sql rename to supabase/migrations/20260926000021_artifact_storage_namespace.sql diff --git a/supabase/migrations/014_cron_checkpoints.sql b/supabase/migrations/20260926000022_cron_checkpoints.sql similarity index 100% rename from supabase/migrations/014_cron_checkpoints.sql rename to supabase/migrations/20260926000022_cron_checkpoints.sql diff --git a/supabase/migrations/014_cron_job_failures.sql b/supabase/migrations/20260926000023_cron_job_failures.sql similarity index 100% rename from supabase/migrations/014_cron_job_failures.sql rename to supabase/migrations/20260926000023_cron_job_failures.sql diff --git a/supabase/migrations/014_email_deliveries.sql b/supabase/migrations/20260926000024_email_deliveries.sql similarity index 100% rename from supabase/migrations/014_email_deliveries.sql rename to supabase/migrations/20260926000024_email_deliveries.sql diff --git a/supabase/migrations/014_job_queue.sql b/supabase/migrations/20260926000025_job_queue.sql similarity index 100% rename from supabase/migrations/014_job_queue.sql rename to supabase/migrations/20260926000025_job_queue.sql diff --git a/supabase/migrations/014_orphaned_artifact_cleanup_audit.sql b/supabase/migrations/20260926000026_orphaned_artifact_cleanup_audit.sql similarity index 100% rename from supabase/migrations/014_orphaned_artifact_cleanup_audit.sql rename to supabase/migrations/20260926000026_orphaned_artifact_cleanup_audit.sql diff --git a/supabase/migrations/014_region_latency_cache.sql b/supabase/migrations/20260926000027_region_latency_cache.sql similarity index 100% rename from supabase/migrations/014_region_latency_cache.sql rename to supabase/migrations/20260926000027_region_latency_cache.sql diff --git a/supabase/migrations/015_job_queue_claim_rpc.sql b/supabase/migrations/20260926000028_job_queue_claim_rpc.sql similarity index 100% rename from supabase/migrations/015_job_queue_claim_rpc.sql rename to supabase/migrations/20260926000028_job_queue_claim_rpc.sql diff --git a/supabase/migrations/016_analytics_rollups.sql b/supabase/migrations/20260926000029_analytics_rollups.sql similarity index 100% rename from supabase/migrations/016_analytics_rollups.sql rename to supabase/migrations/20260926000029_analytics_rollups.sql diff --git a/supabase/migrations/016_template_fulltext_search.sql b/supabase/migrations/20260926000030_template_fulltext_search.sql similarity index 100% rename from supabase/migrations/016_template_fulltext_search.sql rename to supabase/migrations/20260926000030_template_fulltext_search.sql diff --git a/supabase/migrations/017_atomic_provider_connections.sql b/supabase/migrations/20260926000031_atomic_provider_connections.sql similarity index 100% rename from supabase/migrations/017_atomic_provider_connections.sql rename to supabase/migrations/20260926000031_atomic_provider_connections.sql diff --git a/supabase/migrations/017_cron_failure_atomic_increment.sql b/supabase/migrations/20260926000032_cron_failure_atomic_increment.sql similarity index 100% rename from supabase/migrations/017_cron_failure_atomic_increment.sql rename to supabase/migrations/20260926000032_cron_failure_atomic_increment.sql diff --git a/supabase/migrations/017_cron_job_failure_increment_rpc.sql b/supabase/migrations/20260926000033_cron_job_failure_increment_rpc.sql similarity index 100% rename from supabase/migrations/017_cron_job_failure_increment_rpc.sql rename to supabase/migrations/20260926000033_cron_job_failure_increment_rpc.sql diff --git a/supabase/migrations/017_provider_connections_atomic_rpc.sql b/supabase/migrations/20260926000034_provider_connections_atomic_rpc.sql similarity index 100% rename from supabase/migrations/017_provider_connections_atomic_rpc.sql rename to supabase/migrations/20260926000034_provider_connections_atomic_rpc.sql diff --git a/supabase/migrations/018_atomic_deployment_build_cache_hash.sql b/supabase/migrations/20260926000035_atomic_deployment_build_cache_hash.sql similarity index 100% rename from supabase/migrations/018_atomic_deployment_build_cache_hash.sql rename to supabase/migrations/20260926000035_atomic_deployment_build_cache_hash.sql diff --git a/supabase/migrations/018_dlq_reprocess_atomicity.sql b/supabase/migrations/20260926000036_dlq_reprocess_atomicity.sql similarity index 100% rename from supabase/migrations/018_dlq_reprocess_atomicity.sql rename to supabase/migrations/20260926000036_dlq_reprocess_atomicity.sql diff --git a/supabase/migrations/019_fix_auth_audit_logs_rls.sql b/supabase/migrations/20260926000037_fix_auth_audit_logs_rls.sql similarity index 100% rename from supabase/migrations/019_fix_auth_audit_logs_rls.sql rename to supabase/migrations/20260926000037_fix_auth_audit_logs_rls.sql diff --git a/supabase/migrations/020_fee_bump_usage_records.sql b/supabase/migrations/20260926000038_fee_bump_usage_records.sql similarity index 100% rename from supabase/migrations/020_fee_bump_usage_records.sql rename to supabase/migrations/20260926000038_fee_bump_usage_records.sql diff --git a/supabase/migrations/021_payment_idempotency_atomic_key_generation.sql b/supabase/migrations/20260926000039_payment_idempotency_atomic_key_generation.sql similarity index 100% rename from supabase/migrations/021_payment_idempotency_atomic_key_generation.sql rename to supabase/migrations/20260926000039_payment_idempotency_atomic_key_generation.sql diff --git a/supabase/migrations/021_usage_records_atomic_increment_rpc.sql b/supabase/migrations/20260926000040_usage_records_atomic_increment_rpc.sql similarity index 100% rename from supabase/migrations/021_usage_records_atomic_increment_rpc.sql rename to supabase/migrations/20260926000040_usage_records_atomic_increment_rpc.sql diff --git a/supabase/tests/migrations/schema-smoke.sql b/supabase/tests/migrations/schema-smoke.sql new file mode 100644 index 00000000..97f98703 --- /dev/null +++ b/supabase/tests/migrations/schema-smoke.sql @@ -0,0 +1,25 @@ +DO $$ +BEGIN + IF to_regclass('public.profiles') IS NULL THEN + RAISE EXCEPTION 'migration sequence did not create public.profiles'; + END IF; + + IF to_regclass('public.templates') IS NULL THEN + RAISE EXCEPTION 'migration sequence did not create public.templates'; + END IF; + + IF to_regclass('public.deployments') IS NULL THEN + RAISE EXCEPTION 'migration sequence did not create public.deployments'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'profiles' + AND column_name = 'subscription_tier' + ) THEN + RAISE EXCEPTION 'migration sequence did not create profiles.subscription_tier'; + END IF; +END; +$$; \ No newline at end of file