Kulcare Docs
General07 integrations

🔄 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_id fields
  • ✅ Error handling with BaserowError model
  • ✅ Background job processing via Sidekiq

**⚠️ Migration Incomplete:

  • ⚠️ Missing baserow queue 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 service
  • baserow_update_user_stats.rb - Statistics update service
  • baserow_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 delay

2. Legacy Airtable Integration (Backup)

📍 Location: backend/parts/core/services/

  • airtable_update_user.rb - Legacy user service
  • airtable_update_user_stats.rb - Legacy stats service
  • airtable_error.rb - Legacy error model

⚠️ Still Available:

  • Rake Tasks: Manual Airtable operations available
  • Queue: airtable queue 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: Text

Step 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

  1. Login to https://baserow.kulcare.com
  2. Click "+ Create database"
  3. Name: "Kulcare Analytics"
  4. Click "Create database"

2️⃣ Create Production Table

  1. In "Kulcare Analytics" database, click "+ Create table"
  2. Name: "Users - Production"
  3. Click "Create table"
  4. Copy the Table ID from URL: https://baserow.kulcare.com/database/[DB_ID]/table/[PROD_TABLE_ID]
  5. Note: PROD_TABLE_ID = [PROD_TABLE_ID]

3️⃣ Create Staging Table

  1. Click "+ Create table" again
  2. Name: "Users - Staging"
  3. Click "Create table"
  4. Copy the Table ID from URL: https://baserow.kulcare.com/database/[DB_ID]/table/[STAGING_TABLE_ID]
  5. 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

  1. In Baserow: Settings → API Tokens
  2. Create new token with full permissions
  3. Copy Table ID from URL bar when viewing table
  4. 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 table

Step 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 :default

Phase 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_baserow

Phase 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 ID

Step 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_id

Step 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 Baserow

Step 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 processing

Step 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_id mappings

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

ServiceCurrent CostPersonal Instance
Baserow Hosted Advanced$240/year/user$0
Features250K rows, 100GBUnlimited
API LimitsRate limitedNo limits
Data OwnershipThird-partyYour 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 baserow queue 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:

  1. Create table structure in personal Baserow instance
  2. Update Rails credentials to point to personal instance
  3. Add baserow queue to Sidekiq configuration
  4. Test in development environment
  5. Deploy to staging for validation
  6. 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

EnvironmentBaserow TableData SourceUsage
DevelopmentUsers - StagingLocal test dataDevelopment/testing
StagingUsers - StagingStaging server dataPre-production testing
ProductionUsers - ProductionLive user dataProduction 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_id in 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

On this page