🔄 Baserow Migration Guide: From Hosted to Personal Instance
📊 Migration Status Overview
✅ Current State (December 2024)
Migration Progress: 95% Complete - Active Baserow integration in production
**✅ Completed:
- ✅ Baserow services implemented (
BaserowUpdateUser,BaserowUpdateUserStats) - ✅ All model callbacks migrated from Airtable → Baserow
- ✅ Database schema updated with
baserow_record_idfields - ✅ Error handling with
BaserowErrormodel - ✅ Background job processing via Sidekiq
**⚠️ Migration Incomplete:
- ⚠️ Missing
baserowqueue in Sidekiq configuration - ⚠️ Still pointing to hosted Baserow (
api.baserow.io)
🏗 Current Architecture Analysis
1. Baserow Integration (Primary)
📍 Location: backend/parts/core/services/
baserow_update_user.rb- Main user analytics servicebaserow_update_user_stats.rb- Statistics update servicebaserow_error.rb- Error logging model
🔌 Current Configuration:
# Rails credentials:
baserow:
api_url: https://api.baserow.io # ⚠️ Still hosted
api_token: [HOSTED_API_TOKEN] # ⚠️ Hosted token
table_id: [HOSTED_TABLE_ID] # ⚠️ Hosted table📊 Data Schema Tracked:
Core Identity:
- Number (Phone)
- Contact (Phone)
- k-id (User ID)
- Name If Available
- Email ID
Professional Data:
- Speciality
- Experience
- MCI Number
- Education
- Verified Doctor (Yes/No)
- Verified On
Business Intelligence:
- Plan (Subscription)
- Subscription Status
- Plan Expiring On
- Last Subscription Ended On
- City
- Clinic Logo URL
- Public Profile URL
Analytics & Funnel:
- Intent
- Account
- Platform
- Active (Yes/No)
- Funnel Stage:
• Acquisition (Every signed-up user)
• Activation (Verified doctors)
• Revenue (Active subscription)
• Retention (Active < 6 months)
• Referral (Active > 6 months)
• Churn (Expired subscription)
Statistics:
- Patients Count
- Appointments Count
- Careplans Count
- Telemedicines Count
- Updated At🚀 Trigger Points:
# Active Baserow job triggers:
UserAccount.create → BaserowUpdateUser.perform_later(user_id) # 2s delay
Doctor.verification → BaserowUpdateUser.perform_later(user_id) # 30s delay
BillingSubscription → BaserowUpdateUser.perform_later(user_id) # 30s delay
Clinic.update → BaserowUpdateUser.perform_later(user_id) # 30s delay
ClinicLocation.update → BaserowUpdateUser.perform_later(user_id) # 30s delay2. Legacy Airtable Integration (Backup)
📍 Location: backend/parts/core/services/
airtable_update_user.rb- Legacy user serviceairtable_update_user_stats.rb- Legacy stats serviceairtable_error.rb- Legacy error model
⚠️ Still Available:
- Rake Tasks: Manual Airtable operations available
- Queue:
airtablequeue configured in Sidekiq - Cron Job: Removed from production ✅
🔌 Legacy Configuration:
# config/initializers/airrecord.rb
# Rails credentials:
airtable:
api_key: [AIRTABLE_API_KEY]
base_id: [AIRTABLE_BASE_ID]
table_id: [AIRTABLE_TABLE_ID]🎯 Complete Migration Plan
Phase 1: Personal Baserow Instance Setup
Step 1.1: Personal Instance Ready ✅
- URL:
https://baserow.kulcare.com - Status: Running Baserow 1.35.1
- Features: All Enterprise features unlocked
- WebSocket: Fixed and working
Step 1.2: Create Baserow Database & Table Structure
🏗 Action Required: Login to https://baserow.kulcare.com and create:
Database: "Kulcare Analytics"
Tables:
- "Users - Production" (for production environment)
- "Users - Staging" (for staging environment)
Fields to Create:
# Core Identity Fields
Number: Text (Primary contact)
Contact: Text (Phone number)
k-id: Number (User ID from Rails)
Name If Available: Text
Email ID: Email
# Professional Fields
Speciality: Text
Experience: Text
MCI Number: Text
Education: Long Text
Verified Doctor: Single Select (Yes/No/[empty])
Verified On: Date
# Business Fields
Signed up?: Single Select (Yes/No)
Date: Date (Signup date)
Plan: Text
Subscription Status: Single Select (Active/Not Active)
Plan Expiring On: Date
Last Subscription Ended On: Date
# Location & Profile
City: Text
Clinic Logo URL: URL
Public Profile: URL
# Analytics Fields
Intent: Text
Account: Text
Platform: Text
Active: Single Select (Yes/No)
Funnel: Single Select
Options: Acquisition|Activation|Revenue|Retention|Referral|Churn
# Statistics Fields
Patients Count: Number
Appointments Count: Number
Careplans Count: Number
Telemedicines Count: Number
Updated At: Date
# Onboarding Fields (Optional)
O Speciality: Text
O City: Text
O Fees: Text
O Clinic Name: Text
O Interests: Text
O Recommendation: TextStep 1.3: Detailed Table Setup Plan
🎯 Environment Separation Strategy:
- Production Table: Real user data, production analytics
- Staging Table: Test data, development/staging environment
- Shared API Token: Single token with access to both tables
- Environment-specific Table IDs: Different table IDs for staging vs production
📋 Step-by-Step Setup:
1️⃣ Create Database
- Login to
https://baserow.kulcare.com - Click "+ Create database"
- Name: "Kulcare Analytics"
- Click "Create database"
2️⃣ Create Production Table
- In "Kulcare Analytics" database, click "+ Create table"
- Name: "Users - Production"
- Click "Create table"
- Copy the Table ID from URL:
https://baserow.kulcare.com/database/[DB_ID]/table/[PROD_TABLE_ID] - Note: PROD_TABLE_ID =
[PROD_TABLE_ID]
3️⃣ Create Staging Table
- Click "+ Create table" again
- Name: "Users - Staging"
- Click "Create table"
- Copy the Table ID from URL:
https://baserow.kulcare.com/database/[DB_ID]/table/[STAGING_TABLE_ID] - Note: STAGING_TABLE_ID =
[STAGING_TABLE_ID]
4️⃣ Setup Fields in Both Tables ⚠️ Important: You need to create the same field structure in BOTH tables
For "Users - Production" table:
- Create all fields listed in the schema above
- Set appropriate field types (Text, Number, Date, etc.)
For "Users - Staging" table:
- Duplicate the exact same field structure
- Same field names, same field types
- This ensures staging tests match production behavior
5️⃣ Field Creation Order (Recommended)
# Create fields in this order for consistency:
1. Core Identity (Number, Contact, k-id, Name, Email)
2. Professional (Speciality, Experience, MCI, Education, Verified Doctor, Verified On)
3. Business (Signed up, Date, Plan, Subscription Status, Plan Expiring On, etc.)
4. Location & Profile (City, Clinic Logo URL, Public Profile)
5. Analytics (Intent, Account, Platform, Active, Funnel)
6. Statistics (Patients Count, Appointments Count, etc.)
7. Onboarding (O Speciality, O City, etc.)6️⃣ Verify Table Structure
- Both tables should have identical field structures
- Test field creation by adding a sample record to each table
- Ensure Single Select options are consistent between tables
Step 1.4: Generate API Credentials
- In Baserow: Settings → API Tokens
- Create new token with full permissions
- Copy Table ID from URL bar when viewing table
- Note both values for configuration
Phase 2: Rails Configuration Update
Step 2.1: Update Rails Credentials for Both Environments
📋 Credentials Structure:
# Shared configuration for both environments:
baserow:
api_url: https://baserow.kulcare.com # Same for both
api_token: [YOUR_PERSONAL_TOKEN] # Same token for both tables
# Environment-specific table IDs:
production_table_id: [PROD_TABLE_ID] # Production table ID
staging_table_id: [STAGING_TABLE_ID] # Staging table ID🛠 Update Development Credentials:
EDITOR=nano rails credentials:edit# Add to development credentials:
baserow:
api_url: https://baserow.kulcare.com
api_token: [YOUR_PERSONAL_TOKEN]
production_table_id: [PROD_TABLE_ID]
staging_table_id: [STAGING_TABLE_ID]
# Development uses staging table for testing
table_id: [STAGING_TABLE_ID] 🛠 Update Staging Credentials:
EDITOR=nano rails credentials:edit --environment staging# Add to staging credentials:
baserow:
api_url: https://baserow.kulcare.com
api_token: [YOUR_PERSONAL_TOKEN]
table_id: [STAGING_TABLE_ID] # Staging uses staging table🛠 Update Production Credentials:
EDITOR=nano rails credentials:edit --environment production # Add to production credentials:
baserow:
api_url: https://baserow.kulcare.com
api_token: [YOUR_PERSONAL_TOKEN]
table_id: [PROD_TABLE_ID] # Production uses production tableStep 2.2: Update Sidekiq Configuration
📝 Edit: backend/config/sidekiq.yml
# Add baserow queue (line 28):
:queues:
- patient_history
- push_notifications
# ... existing queues ...
- airtable # Keep for backup
- baserow # ← ADD THIS LINE
- geocoding
# ... rest of queues ...Step 2.3: Fix Queue Assignment
📝 Edit: backend/parts/core/services/baserow_update_user.rb
class BaserowUpdateUser < ActiveJob::Base
queue_as :baserow # ← Verify this is :baserow, not :default📝 Edit: backend/parts/core/services/baserow_update_user_stats.rb
class BaserowUpdateUserStats < ActiveJob::Base
queue_as :baserow # ← Verify this is :baserow, not :defaultPhase 3: Migration Completion
Step 3.1: Replace Airtable Cron with Baserow Cron
Status: Airtable cron job removed from production ✅
🔄 Add Baserow weekly sync cron job:
# Add to cron_prod.txt:
0 0 * * sun /bin/bash -lc '. ~/.bashrc; cd /home/deploy/kulcare_api_backend/current/ && RAILS_ENV=production bundle exec rake db:update_baserow' >> /home/deploy/kulcare_api_backend/shared/log/cron_logs/cron_weekly_update_baserow.log 2>&1
# Add to staging cron (if applicable):
0 0 * * sun /bin/bash -lc '. ~/.bashrc; cd /home/deploy/kulcare_api_backend/current/ && RAILS_ENV=staging bundle exec rake db:update_baserow' >> /home/deploy/kulcare_api_backend/shared/log/cron_logs/cron_weekly_update_baserow_staging.log 2>&1📝 This cron job will:
- Run weekly (Sundays at midnight)
- Process all verified users with doctors
- Update/create records in appropriate environment table
- Handle bulk data population automatically
Step 3.2: Create Baserow Rake Tasks
📝 Create: backend/lib/tasks/baserow/weekly_update_baserow.rake
namespace :db do
desc 'Weekly update all user data in Baserow (replaces Airtable cron)'
task update_baserow: :environment do
puts "Starting weekly Baserow update for #{Rails.env} environment..."
puts "Target table: #{Rails.application.credentials.dig(:baserow, :table_id)}"
verified_users = User.verified.includes(:doctor, :clinic, :billing_customer)
puts "Found #{verified_users.count} verified users to update"
success_count = 0
error_count = 0
verified_users.find_each(batch_size: 50) do |user|
next unless user.doctor.present?
begin
puts "Updating user #{user.id} (#{user.doctor.title})"
BaserowUpdateUser.perform_now(user.id)
success_count += 1
sleep(0.1) # Rate limiting to avoid API overload
rescue StandardError => e
puts "Error updating user #{user.id}: #{e.message}"
error_count += 1
end
end
puts "Baserow update completed!"
puts "✅ Success: #{success_count} users"
puts "❌ Errors: #{error_count} users" if error_count > 0
puts "Environment: #{Rails.env}"
puts "Table used: #{Rails.application.credentials.dig(:baserow, :table_id)}"
end
desc 'One-time migration: Populate Baserow with all existing data'
task migrate_to_baserow: :environment do
puts "🚀 Starting one-time Baserow migration for #{Rails.env}..."
puts "This will populate the Baserow table with ALL existing user data"
puts "Target table: #{Rails.application.credentials.dig(:baserow, :table_id)}"
# Clear existing baserow_record_id to ensure fresh sync
puts "Clearing existing baserow_record_id values..."
User.where.not(baserow_record_id: nil).update_all(baserow_record_id: nil)
# Run the same logic as weekly update
Rake::Task['db:update_baserow'].invoke
puts "🎉 Migration completed!"
end
end📋 Usage:
# Weekly cron job will run:
bundle exec rake db:update_baserow
# For one-time initial migration after setup:
bundle exec rake db:migrate_to_baserowPhase 4: Testing & Validation
Step 4.1: Development Testing
Environment: Uses Staging Table
# Rails console testing (development environment)
user = User.first
BaserowUpdateUser.perform_now(user.id)
# Check for errors
BaserowError.recent.each { |e| puts "#{e.user_id}: #{e.error_message}" }
# Verify record creation
user.reload.baserow_record_id # Should return new record ID
# Verify correct table usage
puts "Current environment: #{Rails.env}"
puts "Table ID: #{Rails.application.credentials.dig(:baserow, :table_id)}"
# Should show staging table IDStep 4.2: Staging Environment Testing
Environment: Uses Staging Table
# Deploy to staging first
cap staging deploy
# SSH to staging server
ssh deploy@staging.kulcare.com
# Test on staging
cd /path/to/app
RAILS_ENV=staging rails console
# Test user sync
user = User.verified.first
BaserowUpdateUser.perform_now(user.id)
# Verify staging table receives data
user.reload.baserow_record_idStep 4.3: Production Environment Testing
Environment: Uses Production Table
# Deploy to production (after staging validation)
cap production deploy
# SSH to production server
ssh deploy@production.kulcare.com
# Test on production
cd /path/to/app
RAILS_ENV=production rails console
# Test with a single user first
user = User.verified.where(verified: true).first
BaserowUpdateUser.perform_now(user.id)
# Verify production table receives data
user.reload.baserow_record_id
# Monitor logs
tail -f log/production.log | grep BaserowStep 4.4: Background Job Testing
# Start Sidekiq with baserow queue
bundle exec sidekiq -e development
# Trigger background job
BaserowUpdateUser.perform_later(User.first.id)
# Check Sidekiq web UI for job processingStep 4.5: Environment Validation Checklist
✅ Staging Validation:
- Staging users appear in "Users - Staging" table
- No BaserowError entries for staging environment
- Background jobs process correctly in staging
- Field mappings work correctly
✅ Production Validation:
- Production users appear in "Users - Production" table
- Separate from staging data (no cross-contamination)
- Production background jobs process correctly
- Monitor error logs for API failures
- Verify all user callbacks trigger correctly
✅ Cross-Environment Verification:
- Staging table contains only staging/development data
- Production table contains only production data
- Same field structure in both tables
- API token works for both tables
Phase 5: Cleanup & Optimization
Step 5.1: Data Migration Options
Option A: Fresh Start (Recommended)
- New data flows to personal instance immediately
- Clean analytics from migration date forward
- Old hosted data remains as backup
Option B: Historical Migration
- Export existing data from hosted Baserow
- Import into personal instance
- Update
baserow_record_idmappings
Step 5.2: Performance Monitoring
# Add performance monitoring
class BaserowUpdateUser < ActiveJob::Base
around_perform do |job, block|
start_time = Time.current
block.call
duration = Time.current - start_time
Rails.logger.info "BaserowUpdateUser job completed in #{duration}s"
end
end📋 Benefits of Migration
💰 Cost Analysis
| Service | Current Cost | Personal Instance |
|---|---|---|
| Baserow Hosted Advanced | $240/year/user | $0 |
| Features | 250K rows, 100GB | Unlimited |
| API Limits | Rate limited | No limits |
| Data Ownership | Third-party | Your server |
🚀 Performance Benefits
- Faster API responses (dedicated server vs shared)
- No rate limiting (self-hosted)
- Better uptime control (your infrastructure)
- Custom backup strategies
🔒 Security & Privacy
- Complete data ownership
- No third-party data sharing
- Custom security policies
- GDPR/compliance control
🛡 Rollback Plan
If Issues Occur:
# Quick rollback steps:
1. Revert Rails credentials to hosted values:
baserow:
api_url: https://api.baserow.io
api_token: [ORIGINAL_HOSTED_TOKEN]
table_id: [ORIGINAL_HOSTED_TABLE]
2. Restart application servers
3. Data flow returns to hosted instance
4. No code changes needed (service layer abstracts API)📊 Current Integration Status
✅ Migration Completed
- Baserow service classes implemented
- Model callbacks migrated from Airtable → Baserow
- Database schema with
baserow_record_id - Error handling system
- Background job processing
- Personal Baserow instance deployed
⚠️ Migration Remaining
- Update Rails credentials (Point to personal instance)
- Add
baserowqueue to Sidekiq config - Create Baserow table structure
- Test data flow end-to-end
- Monitor for 1 week before cleanup
🔄 Dual Service Status
# Current state - both services exist:
ActiveServices:
✅ BaserowUpdateUser (queue: :baserow) → api.baserow.io (hosted)
LegacyServices:
📦 AirtableUpdateUser (preserved for backup)
📦 AirtableUpdateUserStats (preserved for backup)
📦 AirtableError (preserved for backup)
📦 Airtable cron job (removed from production ✅)🎯 Next Steps
Immediate Actions Required:
- Create table structure in personal Baserow instance
- Update Rails credentials to point to personal instance
- Add
baserowqueue to Sidekiq configuration - Test in development environment
- Deploy to staging for validation
- Deploy to production with monitoring
Timeline Estimate:
- Setup & Config: 2-3 hours
- Testing & Validation: 1-2 days
- Production Deploy: 1 day
- Monitoring Period: 1 week
- Total: ~2 weeks for complete migration
Success Criteria:
- ✅ New user data appears in personal Baserow instance
- ✅ Existing users get updated records
- ✅ No BaserowError entries in logs
- ✅ Background jobs process successfully
- ✅ Performance equals or exceeds hosted service
📞 Support: If any issues arise during migration, the service layer abstraction allows for quick rollback without code changes - simply revert the Rails credentials configuration.
🎉 Result: Complete migration from expensive hosted Baserow ($240/year/user) to free unlimited personal instance with better performance and full data ownership.
🎯 Dual Environment Setup Summary
📊 Environment Configuration
| Environment | Baserow Table | Data Source | Usage |
|---|---|---|---|
| Development | Users - Staging | Local test data | Development/testing |
| Staging | Users - Staging | Staging server data | Pre-production testing |
| Production | Users - Production | Live user data | Production analytics |
🔧 Configuration Summary
Single Baserow Instance: https://baserow.kulcare.com
- Database: "Kulcare Analytics"
- Table 1: "Users - Production" (Production data)
- Table 2: "Users - Staging" (Staging/dev data)
- API Token: Single token with access to both tables
- Environment Separation: Achieved via different
table_idin Rails credentials
🚀 Benefits of Dual Environment Setup
✅ Data Separation:
- Production analytics remain clean and separate
- Staging tests don't pollute production data
- Safe testing environment for Baserow integration changes
✅ Development Workflow:
- Developers can test Baserow integration without affecting production
- Staging environment mirrors production setup exactly
- Easy validation before production deployment
✅ Cost Efficiency:
- Single Baserow instance handles both environments
- No additional licensing or hosting costs
- Shared infrastructure with logical separation
✅ Operational Benefits:
- Single API token to manage
- Consistent field structure across environments
- Unified monitoring and maintenance
📋 Quick Reference
Development/Staging Table ID: [STAGING_TABLE_ID]
Production Table ID: [PROD_TABLE_ID]
API URL: https://baserow.kulcare.com
API Token: [YOUR_PERSONAL_TOKEN]
Rails Credentials Pattern:
# Each environment gets its own table_id:
development: table_id → STAGING_TABLE_ID
staging: table_id → STAGING_TABLE_ID
production: table_id → PROD_TABLE_ID