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
| Method | Purpose | Returns |
|---|---|---|
find_or_create!(email, first_name:, last_name:, locale:) | Creates or finds a Firebase user, subscribes to newsletters | UserResult |
make_premium!(email, plan:, first_name:, last_name:, plan_monthly_fee:) | Activates premium status in Firestore with user details | DocumentSnapshot |
disable_premium!(email) | Deactivates premium status | DocumentSnapshot |
find_by_email(email) | Looks up user in Firestore | DocumentSnapshot 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
Password Reset Link Generation
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 user@example.com "https://aleteia.org/login?lang=it"
# Output (stdout)
https://aleteia-subscriptions.firebaseapp.com/__/auth/action?mode=resetPassword&oobCode=...
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Invalid arguments |
| 2 | Firebase initialization failed |
| 3 | Link 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.
Method 1: Inline JSON (Recommended for Heroku)
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
| Error | Handling |
|---|---|
UserNotFound | Creates new user |
EmailAlreadyExists | Fetches existing user |
InvalidEmail | Logs error, raises exception |
Firestore Errors
| Error | Handling |
|---|---|
| Document not found | Logs warning, operation skipped |
| Permission denied | Logs error, raises exception |
| Network timeout | Retried by Sidekiq |
Go Binary Errors
| Scenario | Handling |
|---|---|
| Binary not found | Raises Errno::ENOENT |
| Timeout (>30s) | Raises TTY::Command::TimeoutExceeded |
| Non-zero exit | Logs 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/v4module
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!(
'new@example.com',
'John',
'Doe',
'en'
)
expect(result.new_user).to be true
expect(result.password_generation_link).to be_present
end
end
end
end
Security Considerations
- Service Account Scope: Use minimal required permissions
- Email Verification: Set
email_verified: trueon creation to bypass verification - Password Reset TTL: Links expire after the Firebase default (1 hour)
- Continue URL: Restricted to approved domains in Firebase console
- 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 provisionedFirebase user found- Existing user locatedPremium enabled- Premium status activatedPremium disabled- Premium status deactivatedPassword reset link generated- Reset link created
Related Documentation
- Setup Guide - Firebase project configuration
- Overview - System architecture overview