Skip to content

Conversation

@marcodejongh
Copy link
Owner

Summary

Implements a comprehensive migration strategy to sync Aurora climb data (ascents and bids) to the new unified boardsesh_ticks table while maintaining backward compatibility with existing Aurora tables.

This PR enables:

  • Unified storage for all climb attempts (successful and failed)
  • Quality rating conversion from Aurora's scale to Boardsesh's 5-star system
  • Automatic migration for new users linking Aurora credentials
  • Background migration for existing users

Changes

New Files

  • packages/db/drizzle/0026_migrate_aurora_to_boardsesh_ticks.sql - SQL migration to backfill existing Aurora data
  • packages/web/app/lib/data-sync/aurora/migrate-user-history.ts - Reusable function for per-user historical data migration
  • packages/web/app/api/internal/migrate-users-cron/route.ts - Background cron job to process unmigrated users

Modified Files

  • packages/web/app/lib/data-sync/aurora/user-sync.ts - Added dual-write strategy for ascents/bids to both Aurora tables and boardsesh_ticks
  • packages/web/app/api/internal/aurora-credentials/route.ts - Trigger user migration when credentials are added
  • vercel.json - Added daily migration cron job schedule (3 AM)

Implementation Details

Data Transformations

  • Quality Conversion: (aurora_quality / 3.0) * 5 to convert Aurora's 1-5 scale
  • Status Mapping:
    • Aurora ascents with attempt_id = 1status: 'flash'
    • Aurora ascents with attempt_id > 1status: 'send'
    • Aurora bids → status: 'attempt'
  • User Mapping: Aurora user IDs mapped to NextAuth user IDs via aurora_credentials table

Migration Strategy

Three migration paths ensure all users get their data:

  1. Initial SQL Migration: One-time backfill for all existing users with Aurora credentials
  2. User-Triggered: Automatic migration when users add/update Aurora credentials
  3. Background Cron: Daily job (3 AM) processes up to 10 unmigrated users per run

Dual-Write Pattern

The user-sync.ts code now writes to both:

  • Aurora tables (kilter_ascents, kilter_bids, etc.) - for backward compatibility
  • boardsesh_ticks - unified new table

All writes happen in the same transaction to ensure data integrity.

Testing Strategy

Pre-Migration Validation

-- Count records to be migrated
SELECT 'kilter_ascents' as source, COUNT(*) as total
FROM kilter_ascents ka
LEFT JOIN aurora_credentials ac ON ac.aurora_user_id = ka.user_id;

Post-Migration Validation

-- Verify migration counts
SELECT aurora_type, board_type, COUNT(*) as migrated_count
FROM boardsesh_ticks
WHERE aurora_id IS NOT NULL
GROUP BY aurora_type, board_type;

-- Verify quality conversion
SELECT ka.quality as aurora_quality, bt.quality as boardsesh_quality
FROM kilter_ascents ka
INNER JOIN boardsesh_ticks bt ON bt.aurora_id = ka.uuid
LIMIT 10;

Rollback Plan

If issues occur:

-- Rollback: Delete migrated records
DELETE FROM boardsesh_ticks WHERE aurora_id IS NOT NULL;

Sync code changes can be reverted via git. Aurora tables remain unchanged, ensuring no data loss.

Edge Cases Handled

  • ✅ Users without NextAuth accounts: Filtered out via INNER JOIN
  • ✅ NULL quality values: Handled gracefully by conversion function
  • ✅ Duplicate migrations: Prevented by idempotent design (WHERE NOT EXISTS checks)
  • ✅ Transaction failures: Automatic rollback on any error
  • ✅ Missing Aurora credentials: Sync skips ascents/bids gracefully

🤖 Generated with Claude Code

Implements a comprehensive migration strategy to sync Aurora climb data to the new unified boardsesh_ticks table while maintaining backward compatibility.

Changes:
- Add SQL migration (0026) to backfill existing Aurora data
- Implement dual-write strategy in user-sync.ts for ascents/bids
- Create reusable migrate-user-history.ts for per-user migrations
- Add user-triggered migration when linking Aurora credentials
- Add background cron job to migrate users with missing data
- Convert Aurora quality ratings (1-5) using formula: quality / 3.0 * 5
- Map Aurora user IDs to NextAuth user IDs via aurora_credentials
- Ensure all writes are transactional for data integrity

Migration paths:
1. Initial SQL migration for existing users
2. Automatic migration when users add Aurora credentials
3. Daily cron job (3 AM) to catch any missed users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Dec 31, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
boardsesh Ready Ready Preview, Comment Dec 31, 2025 2:50am

@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Issues Found

  1. migrate-user-history.ts:44-48 - Invalid Drizzle where clause syntax. Using (bt) => bt.userId === nextAuthUserId is not valid Drizzle ORM syntax - this will likely throw a runtime error or produce incorrect results. Should use and(eq(boardseshTicks.userId, nextAuthUserId), ...).

  2. migrate-user-history.ts:58-61, 93-96 - Same invalid where clause pattern for ascents and bids queries. This needs Drizzle's eq() helper.

  3. migrate-users-cron/route.ts:25 - Weak authentication bypass: VERCEL_ENV \!== 'development' allows anyone to access the cron endpoint if CRON_SECRET is not set in production. Should check \!CRON_SECRET first and reject.

  4. migrate-user-history.ts - Missing duplicate prevention on inserts. Unlike user-sync.ts which uses onConflictDoUpdate, this file does raw inserts that could fail on duplicate auroraId if migration runs concurrently.

  5. Duplicated code: convertQuality() function is defined identically in both migrate-user-history.ts:13-16 and user-sync.ts:16-19. Should be extracted to a shared utility.

  6. SQL migration (0026) - Quality conversion formula comment says "Aurora quality (1-5)" but the formula quality / 3.0 * 5 suggests Aurora uses 0-3 scale, not 1-5. This inconsistency could lead to incorrect data.

  7. user-sync.ts:502 - Passing empty string as nextAuthUserId when null (nextAuthUserId || '') could insert invalid data into boardsesh_ticks.userId for tables other than ascents/bids that slip through.

Fixed TypeScript errors in migrate-user-history.ts by replacing boolean expressions in .where() clauses with proper Drizzle ORM SQL helper functions (eq, and, isNotNull).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Issues Found

  1. Performance: N+1 query pattern (migrate-user-history.ts:70-128) - Individual INSERT for each ascent/bid in a loop. For users with hundreds of records, this will be very slow. Consider batch inserts.

  2. Duplicate code (migrate-user-history.ts:14-17 and user-sync.ts:16-19) - convertQuality function is duplicated. Extract to a shared utility.

  3. Auth bypass in development (migrate-users-cron/route.ts:25) - VERCEL_ENV !== "development" allows unauthenticated access when VERCEL_ENV is undefined (not just in dev). Should use explicit check: process.env.VERCEL_ENV === "development".

  4. SQL injection risk (migrate-users-cron/route.ts:36-50) - Raw SQL query is safe here, but using Drizzle query builder would be more consistent with the rest of the codebase.

  5. Unused import (migrate-users-cron/route.ts:5) - eq is imported but never used.

  6. Silent skip behavior (migrate-user-history.ts:56-61) - If a user has any migrated tick, all remaining records are skipped. This prevents incremental migration of new ascents/bids after initial migration.

  7. Quality conversion formula mismatch - Comment says "Aurora quality (1-5) to Boardsesh quality (1-5)" but formula (x / 3.0) * 5 converts 3 to 5, 5 to 8.3 (clamped). The SQL migration uses same formula. If Aurora is already 1-5, this seems incorrect.

  8. Unused NeonDatabase type import (migrate-user-history.ts:4) - NeonDatabase is imported but never used.

Fixed type mismatches in migration and sync code:
- Removed ::text casts from timestamp columns in SQL migration
- Changed TypeScript to use Date objects instead of .toISOString()
- Ensures proper timestamp type compatibility with PostgreSQL schema

This fixes the error: "column climbed_at is of type timestamp without time zone but expression is of type text"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Critical Issues

  1. onConflictDoUpdate on non-unique column will fail - packages/web/app/lib/data-sync/aurora/user-sync.ts:217-218, 290-291: The code uses .onConflictDoUpdate({ target: boardseshTicks.auroraId, ... }) but auroraId is not marked as unique in the schema (packages/db/src/schema/app/ascents.ts:65). PostgreSQL requires a unique constraint for ON CONFLICT. This will cause runtime errors.

Other Issues

  1. Duplicate convertQuality function - packages/web/app/lib/data-sync/aurora/user-sync.ts:16-19 and packages/web/app/lib/data-sync/aurora/migrate-user-history.ts:14-17: Same function is duplicated in two files. Should be extracted to a shared utility.

  2. Unused import - packages/web/app/lib/data-sync/aurora/migrate-user-history.ts:4: NeonDatabase is imported but never used.

  3. Potential N+1 query issue - packages/web/app/lib/data-sync/aurora/migrate-user-history.ts:70-96, 105-128: Individual inserts in a loop for each ascent/bid. For users with many records, this will be slow. Consider using batch inserts.

  4. Cron auth bypass in development - packages/web/app/api/internal/migrate-users-cron/route.ts:25: Auth is bypassed when VERCEL_ENV !== development, but this check might not work as expected locally (VERCEL_ENV may be undefined, not development).

Fixed TypeScript type errors by using .toISOString() for timestamp fields in Drizzle inserts. The boardsesh_ticks schema uses { mode: 'string' } for timestamps, requiring string values instead of Date objects.

Changes:
- Convert Date objects to ISO strings for all timestamp fields
- Applies to both migrate-user-history.ts and user-sync.ts
- SQL migration remains correct (uses PostgreSQL timestamp types)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Issues Found

  1. Duplicate convertQuality function - Same function defined in both migrate-user-history.ts:14 and user-sync.ts:16. Extract to shared utility.

  2. Performance: N+1 query pattern in migration - migrate-user-history.ts:70-96 and migrate-user-history.ts:105-128 insert records one-by-one in a loop. For users with many ascents/bids, this creates thousands of individual INSERT statements. Use batch inserts instead.

  3. Weak auth bypass in cron endpoint - migrate-users-cron/route.ts:25 checks VERCEL_ENV !== 'development' but VERCEL_ENV can be undefined in non-Vercel environments, bypassing auth. Consider explicit check: process.env.VERCEL_ENV === 'development'.

  4. Race condition potential - migrate-user-history.ts:45-61 checks for existing ticks then inserts, but without row-level locking. Concurrent calls (cron + credential add) could cause duplicate migrations.

  5. Silent data loss - user-sync.ts:497-500 skips ascents/bids sync entirely when nextAuthUserId is null, but still writes to other Aurora tables. The function returns success without indicating the partial sync.

  6. Missing index likely needed - The cron query at migrate-users-cron/route.ts:36-50 joins on bt.aurora_id IS NOT NULL. Ensure boardsesh_ticks.aurora_id has an index for this NOT EXISTS subquery.

  7. Unused import - migrate-user-history.ts:4: NeonDatabase is imported but never used.

Implemented multiple improvements based on code review:

1. **Extract duplicate function**: Created shared convert-quality.ts utility
   - Removes duplicate convertQuality function from migrate-user-history.ts and user-sync.ts
   - Centralizes quality conversion logic (Aurora 1-3 scale → Boardsesh 1-5 scale)

2. **Fix N+1 query pattern**: Use batch inserts for migration
   - Collect all ascent/bid values before inserting
   - Single INSERT with multiple values instead of loop with individual INSERTs
   - Significantly improves performance for users with many climbs

3. **Fix weak auth bypass**: Strengthen cron endpoint security
   - Change from `VERCEL_ENV !== 'development'` to explicit check
   - Now requires secret in all non-development environments
   - Prevents bypass when VERCEL_ENV is undefined

4. **Fix race condition**: Add PostgreSQL advisory locks
   - Use pg_try_advisory_xact_lock to prevent concurrent migrations
   - Hash user ID + board type for unique lock ID
   - Ensures only one migration runs per user+board at a time

5. **Fix silent data loss**: Guard boardsesh_ticks writes
   - Skip dual-write to boardsesh_ticks when nextAuthUserId is null
   - Prevents failed inserts and maintains data consistency
   - Aurora tables still sync correctly for users without NextAuth accounts

6. **Remove unused import**: Clean up NeonDatabase import from migrate-user-history.ts

7. **Verify index exists**: Confirmed syncPendingIdx covers aurora_id queries

Additional improvements:
- Handle null timestamp values from Aurora tables gracefully
- Add validation to skip records with missing required fields
- Improve error messages and logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Issues Found

  1. Weak advisory lock hash - migrate-user-history.ts:36: The lock hash uses char code sum which has high collision probability (e.g., "ab" and "ba" produce same hash). Use a proper hash function or bigint from UUID.

  2. Missing unique constraint on auroraId - The dual-write in user-sync.ts:210-211 uses .onConflictDoUpdate({ target: boardseshTicks.auroraId }) but auroraId in the schema (packages/db/src/schema/app/ascents.ts:65) has no unique constraint - only uuid does. This will fail at runtime.

  3. Potential crash on null created_at - user-sync.ts:279: new Date(item.created_at).toISOString() will throw if created_at is null/undefined (unlike the ascents case which has a fallback on line 204).

  4. Unused import - migrate-users-cron/route.ts:5: eq import is never used.

  5. Misleading variable name - migrate-user-history.ts:49: The query returns { count: id } which is the id column, not an actual count. The logic works but the naming is confusing.

  6. SQL migration missing unique index - The SQL migration in 0026_migrate_aurora_to_boardsesh_ticks.sql inserts with aurora_id but the table needs a unique index on aurora_id for the upsert logic in the application code to work correctly.

…bility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Dec 31, 2025

Claude Review

Issues Found

  1. Weak advisory lock hash collision risk (migrate-user-history.ts:36): The lock hash is just a sum of character codes, which could easily collide (e.g., "abc" and "bca" produce the same hash). Consider using a proper hash function like crypto.createHash('sha256') and taking a portion of it.

  2. Inconsistent null handling for bids createdAt (user-sync.ts:279): Bids use new Date(item.created_at).toISOString() without null check, while ascents at line 204 use item.created_at ? new Date(...) : new Date().toISOString(). If item.created_at is null/undefined for bids, this will throw.

  3. Security: Missing CRON_SECRET validation (migrate-users-cron/route.ts:27): If CRON_SECRET is undefined in production, the auth check authHeader !== \Bearer ${CRON_SECRET}`could allow any request with headerBearer undefinedto pass. Should validateCRON_SECRET` is set before comparing.

  4. Potential race condition between sync and migrate (aurora-credentials/route.ts:211-218): syncUserData and migrateUserAuroraHistory are called sequentially but sync writes to boardsesh_ticks via dual-write. If both run, you could get duplicates since migrate checks for existing ticks before sync's dual-write completes but after sync starts.

  5. Missing index for performance (SQL migration): The WHERE NOT EXISTS (SELECT 1 FROM boardsesh_ticks bt WHERE bt.aurora_id = ...) queries will be slow at scale if aurora_id isn't indexed. Verify boardsesh_ticks.aurora_id has an index.

  6. Unused import (migrate-users-cron/route.ts:5): eq from drizzle-orm is imported but never used (the query uses raw SQL instead).

@marcodejongh marcodejongh merged commit e9af490 into main Dec 31, 2025
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants