Skip to main content

Firebase Integration

The Premium system uses Firebase for user authentication and Firestore for storing premium status. This document covers the integration architecture, authentication flows, and data management.

Architecture

Components

Premium::UsersService

The main service handling all Firebase operations. Located at app/services/premium/users_service.rb.

Key Methods

MethodPurposeReturns
find_or_create!(email, first_name:, last_name:, locale:)Creates or finds a Firebase user, subscribes to newslettersUserResult
make_premium!(email, plan:, first_name:, last_name:, plan_monthly_fee:)Activates premium status in Firestore with user detailsDocumentSnapshot
disable_premium!(email)Deactivates premium statusDocumentSnapshot
find_by_email(email)Looks up user in FirestoreDocumentSnapshot or nil

ResetLinkGenerator (Go Binary)

A Go binary that generates Firebase password reset links. Required because the Ruby Firebase SDK doesn't support PasswordResetLinkWithSettings().

Location: firebase-link/main.go
Compiled Binary: bin/firebase-link

User Creation Flow

Newsletter Auto-Subscription

When a new Firebase user is created, they are automatically subscribed to default newsletter segments for their locale.

Subscribed Segments

New premium users are automatically enrolled in:

  • daily: Daily newsletter
  • vatican: Vatican news updates

Implementation

def subscribe_to_default_segments(email:, first_name:, last_name:, locale:)
multi_request = Subscriptions::MultiRequest.new(
email:,
first_name:,
last_name:,
locale:,
premium: true,
partners: false,
ip_address: '0.0.0.0', # Automatic subscription on user creation
segments: { 'daily' => true, 'vatican' => true }
)

unless multi_request.valid?
logger.error('Failed to subscribe to default segments',
email:,
errors: multi_request.errors.full_messages
)
return
end

multi_request.process
logger.info('Default segments subscription processed',
email:,
segments: %w[daily vatican]
)
end

Subscription Flow

Firestore Operations

Document Structure

Users are stored in the users collection with their Firebase Auth UID as the document ID:

users/
└── {firebase_uid}/
├── email: string
├── display_name: string
├── first_name: string (from Stripe customer metadata)
├── last_name: string (from Stripe customer metadata)
├── premium: boolean
├── plan: string (tier: essential, integral, etc.)
├── plan_name: string (display name)
├── plan_monthly_fee: float (subscription fee in decimal)
├── premium_started_at: timestamp
├── premium_ended_at: timestamp
├── created_at: timestamp
└── updated_at: timestamp

Premium Activation

Premium Deactivation

The Go binary handles password reset link generation because the Ruby Firebase Admin SDK lacks this functionality.

Go Binary Interface

# Usage
bin/firebase-link <email> <continue_url>

# Example
bin/firebase-link [email protected] "https://aleteia.org/login?lang=it"

# Output (stdout)
https://aleteia-subscriptions.firebaseapp.com/__/auth/action?mode=resetPassword&oobCode=...

Exit Codes

CodeMeaning
0Success
1Invalid arguments
2Firebase initialization failed
3Link generation failed

Ruby Integration

class ResetLinkGenerator
def generate(email, locale:)
result = TTY::Command.new.run(
RESET_LINK_BINARY,
email,
continue_url,
timeout: 30
)
link = result.out.strip
add_custom_params(link, locale)
end

private

# Add locale and userMode parameters to the password reset URL
def add_custom_params(url, locale)
uri = Addressable::URI.parse(url)
uri.query_values = (uri.query_values.except('lang', 'userMode') || {}).merge(
lang: locale,
userMode: 'setPassword'
)
uri.to_s
end

def continue_url
ENV.fetch('FIREBASE_CONTINUE_URL', 'https://aleteia.org/login')
end
end

URL Parameters:

  • lang: User's locale (e.g., 'it', 'en', 'fr')
  • userMode=setPassword: Indicates the user is coming from premium subscription flow for frontend detection

Authentication Methods

The service supports multiple credential loading methods for flexibility across environments.

GOOGLE_APPLICATION_CREDENTIALS='{"type":"service_account","project_id":"aleteia-subscriptions",...}'

The service detects JSON content and writes it to a temporary file.

Method 2: File Path

GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json

Method 3: Separate Variables

GOOGLE_PROJECT_ID=aleteia-subscriptions
GOOGLE_CLIENT_EMAIL=premium@aleteia-subscriptions.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"

Credential Loading Flow

Error Handling

Firebase Auth Errors

ErrorHandling
UserNotFoundCreates new user
EmailAlreadyExistsFetches existing user
InvalidEmailLogs error, raises exception

Firestore Errors

ErrorHandling
Document not foundLogs warning, operation skipped
Permission deniedLogs error, raises exception
Network timeoutRetried by Sidekiq

Go Binary Errors

ScenarioHandling
Binary not foundRaises Errno::ENOENT
Timeout (>30s)Raises TTY::Command::TimeoutExceeded
Non-zero exitLogs error, returns nil

Development Setup

Local Firestore Emulator

For local development, use the Firestore emulator:

# docker-compose.yml
firestore:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
command: gcloud emulators firestore start --host-port=0.0.0.0:8000
ports:
- '8000:8000'
environment:
- FIRESTORE_PROJECT_ID=aleteia-subscriptions

Configure Rails to use the emulator:

FIRESTORE_EMULATOR_HOST=localhost:8000

Compiling the Go Binary

cd firebase-link
./compile-firebase
# Binary created at: bin/firebase-link

Requirements:

  • Go 1.21+
  • firebase.google.com/go/v4 module

Testing

RSpec Setup

# spec/support/firestore.rb
RSpec.configure do |config|
config.before(:each, :firestore) do
# Clear Firestore emulator before each test
clear_firestore_emulator
end
end

Test Examples

describe Premium::UsersService do
describe '#find_or_create!' do
context 'when user does not exist' do
it 'creates a new Firebase user' do
result = service.find_or_create!(
'[email protected]',
'John',
'Doe',
'en'
)

expect(result.new_user).to be true
expect(result.password_generation_link).to be_present
end
end
end
end

Security Considerations

  1. Service Account Scope: Use minimal required permissions
  2. Email Verification: Set email_verified: true on creation to bypass verification
  3. Password Reset TTL: Links expire after the Firebase default (1 hour)
  4. Continue URL: Restricted to approved domains in Firebase console
  5. Credentials Storage: Never commit credentials; use environment variables

Monitoring

The service uses SemanticLogger for structured logging:

logger.info("Firebase user created", 
email: email,
uid: user.uid,
locale: locale
)

Key log events:

  • Firebase user created - New user provisioned
  • Firebase user found - Existing user located
  • Premium enabled - Premium status activated
  • Premium disabled - Premium status deactivated
  • Password reset link generated - Reset link created