Firebase Storage Setup Guide
This document explains how to properly configure Firebase Storage for the Eli Health mobile app in any environment (dev/staging/production).
Overview
Firebase Storage is used by the mobile app to upload hormone test images. The images are temporarily stored in Firebase Storage, then processed by the backend API, and finally deleted. This guide explains the complete setup process required to make Firebase Storage work correctly.
Why This Setup is Necessary
Firebase Storage is not just a regular Google Cloud Storage (GCS) bucket. It requires:
- A specific bucket naming convention (
.appspot.com) - Special service accounts with specific roles
- Security rules that control client-side access
- Proper linking between Firebase and GCS
Simply creating a GCS bucket is not sufficient - Firebase Storage requires additional configuration to enable client SDK uploads with authentication.
Architecture
How It Works
Upload Flow
- User Authentication: User signs in via Firebase Auth, receives an auth token containing their UID
- Image Capture: Mobile app captures hormone test image
- Storage Upload: Firebase Storage SDK uploads image to
tempImages/{userId}/{date}/filename.jpg - Security Check: Firebase Storage Rules verify:
- User is authenticated (
request.auth != null) - User is uploading to their own folder (
request.auth.uid == userId)
- User is authenticated (
- Storage: File is stored in GCS bucket
{project-id}.appspot.com - Backend Processing: Backend API (using Admin SDK) reads the file, processes it, and deletes it
Security Layers
Firebase Storage has two independent security layers:
-
Firebase Storage Rules (Client-side control)
- Controls what authenticated mobile users can read/write
- Evaluated for every client SDK request
- Syntax: Firebase Rules language
- Example: Users can only write to
tempImages/{their-uid}/
-
IAM Permissions (Server-side control)
- Controls what service accounts and server-side code can access
- Bypasses Firebase Storage Rules
- Used by Backend API via Admin SDK
- Example: Firebase Admin SDK has
storage.adminrole
Service Accounts
Three service accounts are involved:
| Service Account | Created By | Purpose | Roles |
|---|---|---|---|
firebase-adminsdk-{suffix}@{project}.iam.gserviceaccount.com | Firebase Console | Backend API access via Admin SDK | storage.admin, firebase.sdkAdminServiceAgent |
service-{number}@gcp-sa-firebasestorage.iam.gserviceaccount.com | Firebase Storage linking | Firebase Storage system operations | firebasestorage.serviceAgent |
{project}@appspot.gserviceaccount.com | App Engine initialization | App Engine default service account | objectAdmin on bucket |
Complete Setup Steps
Follow these steps in order to properly configure Firebase Storage for a new environment:
1. Initialize App Engine (Creates the .appspot.com bucket)
App Engine initialization automatically creates the default .appspot.com bucket that Firebase expects.
# Replace 'eli-health-staging' with your project ID
# Replace 'us-central' with your desired region
gcloud app create --region=us-central --project=eli-health-staging
Important Notes:
- This operation is irreversible - you cannot change the region later
- Common regions:
us-central,us-east1,europe-west1 - This creates the bucket:
gs://[PROJECT_ID].appspot.com
Verification:
# Check that the .appspot.com bucket was created
gcloud storage ls --project=eli-health-staging | grep appspot
# Should show: gs://eli-health-staging.appspot.com/
2. Enable Firebase Storage API
gcloud services enable firebasestorage.googleapis.com --project=eli-health-staging
Verification:
gcloud services list --enabled --project=eli-health-staging | grep firebasestorage
# Should show: firebasestorage.googleapis.com
3. Link the Bucket to Firebase Storage
This is the critical step that creates the Firebase Storage service account and properly links the bucket.
# Get an auth token
TOKEN=$(gcloud auth print-access-token)
# Link the bucket to Firebase Storage
curl -X POST \
"https://firebasestorage.googleapis.com/v1beta/projects/eli-health-staging/buckets/eli-health-staging.appspot.com:addFirebase" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: eli-health-staging" \
-H "Content-Type: application/json"
# Expected response:
# {
# "name": "projects/eli-health-staging/buckets/eli-health-staging.appspot.com"
# }
What this does:
- Creates the Firebase Storage service account:
service-[PROJECT_NUMBER]@gcp-sa-firebasestorage.iam.gserviceaccount.com - Grants it the
roles/firebasestorage.serviceAgentrole - Links the bucket to Firebase Storage system
Verification:
# Wait 10 seconds for propagation
sleep 10
# Check that the Firebase Storage service account was created
gcloud projects get-iam-policy eli-health-staging \
--flatten="bindings[].members" \
--format="value(bindings.members)" | grep firebasestorage
# Should show: serviceAccount:service-[NUMBER]@gcp-sa-firebasestorage.iam.gserviceaccount.com
4. Grant Firebase Admin SDK Storage Permissions
The Firebase Admin SDK service account needs storage admin permissions.
# Find the Firebase Admin SDK service account
gcloud iam service-accounts list --project=eli-health-staging | grep firebase-adminsdk
# Grant storage.admin role (replace the email with your actual service account)
gcloud projects add-iam-policy-binding eli-health-staging \
--member="serviceAccount:firebase-adminsdk-klcja@eli-health-staging.iam.gserviceaccount.com" \
--role="roles/storage.admin"
Verification:
# Check the Firebase Admin SDK has storage.admin role
gcloud projects get-iam-policy eli-health-staging \
--flatten="bindings[].members" \
--filter="bindings.members:firebase-adminsdk*" \
--format="table(bindings.role)" | grep storage.admin
# Should show: roles/storage.admin
5. Configure Bucket-Level IAM Permissions
Grant the Firebase Admin SDK service account direct access to the bucket.
# Get your Firebase Admin SDK service account email
FIREBASE_SA=$(gcloud iam service-accounts list --project=eli-health-staging \
--filter="email:firebase-adminsdk*" --format="value(email)")
echo "Firebase Admin SDK: $FIREBASE_SA"
# Grant objectAdmin permission
gsutil iam ch serviceAccount:$FIREBASE_SA:objectAdmin \
gs://eli-health-staging.appspot.com/
# Grant legacyBucketOwner permission
gsutil iam ch serviceAccount:$FIREBASE_SA:legacyBucketOwner \
gs://eli-health-staging.appspot.com/
# Also grant to App Engine service account
gsutil iam ch serviceAccount:eli-health-staging@appspot.gserviceaccount.com:objectAdmin \
gs://eli-health-staging.appspot.com/
Verification:
# Check bucket IAM policy
gsutil iam get gs://eli-health-staging.appspot.com/
# Should show firebase-adminsdk service account with:
# - roles/storage.legacyBucketOwner
# - roles/storage.objectAdmin
6. Deploy Firebase Storage Security Rules
Create and deploy security rules that control client-side access.
Step 6a: Create the rules file
cat > /tmp/storage.rules << 'EOF'
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Allow authenticated users to upload to tempImages folder only
match /tempImages/{userId}/{allPaths=**} {
allow write: if request.auth != null
&& request.auth.uid == userId;
// No read access from client SDK (server reads via Admin SDK)
allow read: if false;
}
// Deny all other access
match /{allPaths=**} {
allow read, write: if false;
}
}
}
EOF
Step 6b: Create the ruleset
TOKEN=$(gcloud auth print-access-token)
# Create JSON payload
python3 << 'PYTHON_EOF'
import json
with open('/tmp/storage.rules', 'r') as f:
rules_content = f.read()
payload = {
"source": {
"files": [
{
"name": "storage.rules",
"content": rules_content
}
]
}
}
with open('/tmp/ruleset_payload.json', 'w') as f:
json.dump(payload, f)
print("✅ Payload created")
PYTHON_EOF
# Upload the ruleset
curl -X POST \
"https://firebaserules.googleapis.com/v1/projects/eli-health-staging/rulesets" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: eli-health-staging" \
-H "Content-Type: application/json" \
-d @/tmp/ruleset_payload.json
# The response will contain a "name" field like:
# "name": "projects/eli-health-staging/rulesets/c70b6500-a5fb-422c-9af3-334b8287f74b"
# Extract the ruleset ID (the part after "rulesets/") for step 6c below
Step 6c: Deploy the ruleset
# Replace RULESET_ID with the ID from step 6b
RULESET_ID="c70b6500-a5fb-422c-9af3-334b8287f74b"
curl -X POST \
"https://firebaserules.googleapis.com/v1/projects/eli-health-staging/releases" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: eli-health-staging" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"projects/eli-health-staging/releases/firebase.storage/eli-health-staging.appspot.com\",
\"rulesetName\": \"projects/eli-health-staging/rulesets/$RULESET_ID\"
}"
Verification:
# Check that rules are deployed
TOKEN=$(gcloud auth print-access-token)
curl -s "https://firebaserules.googleapis.com/v1/projects/eli-health-staging/releases" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: eli-health-staging"
# Should show a release for: firebase.storage/eli-health-staging.appspot.com
7. Test the Setup
Create a test script to verify everything works:
# Create service account key (for testing only)
gcloud iam service-accounts keys create /tmp/test-service-account.json \
--iam-account=firebase-adminsdk-klcja@eli-health-staging.iam.gserviceaccount.com \
--project=eli-health-staging
# Install firebase-admin
cd /tmp
npm install firebase-admin --no-save
# Create test script
cat > test_upload.js << 'EOF'
const admin = require('firebase-admin');
const serviceAccount = require('/tmp/test-service-account.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
storageBucket: 'eli-health-staging.appspot.com'
});
async function test() {
try {
// Get a test user
const email = 'chip+stage@eli.health';
const user = await admin.auth().getUserByEmail(email);
console.log('✅ User UID:', user.uid);
// Upload test file
const bucket = admin.storage().bucket();
const path = `tempImages/${user.uid}/test_${Date.now()}.jpg`;
await bucket.file(path).save('Test data');
console.log('✅ Upload successful!');
console.log('Path:', path);
} catch (error) {
console.error('❌ Error:', error.message);
}
}
test();
EOF
# Run test
node test_upload.js
# Clean up
rm /tmp/test-service-account.json
Quick Reference: Production Setup
For production (eli-health-prod), run these commands in sequence:
# 1. Initialize App Engine
gcloud app create --region=us-central --project=eli-health-prod
# 2. Enable Firebase Storage API
gcloud services enable firebasestorage.googleapis.com --project=eli-health-prod
# 3. Link bucket to Firebase Storage
TOKEN=$(gcloud auth print-access-token)
curl -X POST \
"https://firebasestorage.googleapis.com/v1beta/projects/eli-health-prod/buckets/eli-health-prod.appspot.com:addFirebase" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Goog-User-Project: eli-health-prod" \
-H "Content-Type: application/json"
# 4. Wait for service account creation
sleep 10
# 5. Grant Firebase Admin SDK permissions
FIREBASE_SA=$(gcloud iam service-accounts list --project=eli-health-prod \
--filter="email:firebase-adminsdk*" --format="value(email)")
gcloud projects add-iam-policy-binding eli-health-prod \
--member="serviceAccount:$FIREBASE_SA" \
--role="roles/storage.admin"
# 6. Grant bucket-level permissions
gsutil iam ch serviceAccount:$FIREBASE_SA:objectAdmin \
gs://eli-health-prod.appspot.com/
gsutil iam ch serviceAccount:$FIREBASE_SA:legacyBucketOwner \
gs://eli-health-prod.appspot.com/
gsutil iam ch serviceAccount:eli-health-prod@appspot.gserviceaccount.com:objectAdmin \
gs://eli-health-prod.appspot.com/
# 7. Deploy storage rules (follow steps 6a-6c above, replacing project ID)
Common Issues and Solutions
Issue: "Service account is missing necessary permissions" (412 error)
Cause: The Firebase Storage service account wasn't created or the bucket wasn't linked to Firebase.
Solution: Run step 3 (Link the Bucket to Firebase Storage)
Issue: "storage/object-not-found"
Cause: The .appspot.com bucket doesn't exist.
Solution: Run step 1 (Initialize App Engine)
Issue: Client uploads fail with permission denied
Cause: Firebase Storage security rules are not deployed or are too restrictive.
Solution:
- Check rules exist:
curl -s "https://firebaserules.googleapis.com/v1/projects/PROJECT_ID/releases" ... - Verify rules allow uploads to
tempImages/{userId}/ - Deploy rules using step 6
Issue: Cannot create .appspot.com bucket manually
Cause: .appspot.com buckets require domain verification and can only be created by App Engine/Firebase.
Solution: Always use App Engine initialization (step 1) or Firebase Storage linking (step 3)
Architecture Overview
How Firebase Storage Works
-
Client Upload Flow:
- Mobile app authenticates user with Firebase Auth
- Gets auth token (includes user UID)
- Uploads file to Firebase Storage
- Firebase Storage checks security rules
- If rules allow, file is stored in GCS bucket
-
Security Layers:
- Firebase Storage Rules: Control client-side access (what users can read/write)
- IAM Permissions: Control server-side/service account access
- Bucket IAM: Additional GCS-level permissions
-
Service Accounts:
firebase-adminsdk-[SUFFIX]@[PROJECT].iam.gserviceaccount.com: Used by Firebase Admin SDK (server-side)service-[NUMBER]@gcp-sa-firebasestorage.iam.gserviceaccount.com: Firebase Storage system account[PROJECT]@appspot.gserviceaccount.com: App Engine default service account
File Paths in Our App
Mobile app uploads follow this pattern:
tempImages/{userId}/{date}/{hormoneType}_{timestamp}_{guid}.jpg
Example:
tempImages/r0HJ92dqnPSoNDsREcZeHB1ACn92/2025-10-02/cortisol_1759370776776_5up1iky.jpg
Server processes these files and then deletes them from tempImages/ after analysis.
Verification Checklist
Before considering the setup complete, verify:
- Bucket exists:
gcloud storage ls | grep [PROJECT_ID].appspot.com - Firebase Storage API enabled:
gcloud services list --enabled | grep firebasestorage - Firebase Storage service account exists:
gcloud projects get-iam-policy [PROJECT] | grep gcp-sa-firebasestorage - Firebase Admin SDK has storage.admin:
gcloud projects get-iam-policy [PROJECT] | grep storage.admin - Bucket IAM includes service accounts:
gsutil iam get gs://[BUCKET]/ - Storage rules deployed:
curl ... /releases - Test upload succeeds from server-side script
- Test upload succeeds from mobile app
Mobile App Configuration
The mobile app uses the Firebase Storage bucket configured in the Firebase configuration files:
- iOS:
ios/Eli/{Environment}/GoogleService-Info.plist - Android:
android/app/src/{environment}/google-services.json
Each environment (Dev/Staging/Prod) has its own configuration file that specifies:
<key>STORAGE_BUCKET</key>
<string>eli-health-{environment}.appspot.com</string>
The mobile app code automatically reads this configuration and uses the correct bucket. No code changes are needed when setting up a new environment - only the infrastructure setup described in this guide.
File Upload Pattern
The mobile app uploads files following this pattern:
tempImages/{userId}/{date}/{hormoneType}_{timestamp}_{guid}.jpg
Example:
tempImages/r0HJ92dqnPSoNDsREcZeHB1ACn92/2025-10-02/cortisol_1759370776776_5up1iky.jpg
The tempImages/ prefix is important - it must match the Firebase Storage Rules pattern.
References
- Firebase Storage Documentation
- Firebase Storage Security Rules
- GCP Storage IAM
- Firebase Storage FAQ
- Firebase Rules API Reference
Note: The mobile app reads the bucket name from GoogleService-Info.plist (iOS) and google-services.json (Android). No app code changes are needed — only this infrastructure setup.