Build Astrology App with Firebase and Vedika API
FULL TUTORIAL

Build a Full-Stack Astrology App with Firebase + Vedika API

Firebase Auth, Firestore, Cloud Functions, and Vedika API. A complete tutorial from project setup to deployment.

February 10, 2026 - 25 min read

Most astrology app tutorials stop at "call an API and show the result." That works for a demo, but production apps need authentication, persistent user profiles, server-side API key security, response caching to control costs, and a deployment pipeline. Firebase handles all of that infrastructure. Vedika API handles the astrology.

This tutorial walks through building a complete astrology application using React, Firebase (Auth, Firestore, Cloud Functions, Hosting), and Vedika API for birth chart calculations and AI-powered interpretations. Every code block is copy-paste ready. By the end, you will have a deployed app where users can sign up, save birth profiles, generate Vedic birth charts with Swiss Ephemeris precision, and ask an AI astrologer questions about their chart.

What You Will Build

  • Firebase Auth for email/password user registration and login
  • Firestore to store and retrieve birth profiles per user
  • Cloud Functions for server-side Vedika API calls (API key never touches the browser)
  • Vedika V2 endpoints for birth chart calculations with 120+ data points
  • Vedika AI chat for natural language astrology Q&A based on a user's birth chart
  • Response caching in Firestore to avoid paying for the same chart twice
  • Firebase Hosting for one-command deployment

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ installed
  • A Google account (for Firebase)
  • A Vedika API key from vedika.io/console (or use the free sandbox during development)
  • Basic familiarity with React and JavaScript

1 Project Setup

Create a new React app and install dependencies:

# Create React app
npx create-react-app astro-app
cd astro-app

# Install Firebase SDK and Vedika SDK
npm install firebase @vedika-io/sdk

# Install Firebase CLI globally (if you don't have it)
npm install -g firebase-tools

# Login to Firebase
firebase login

# Initialize Firebase in your project
firebase init

During firebase init, select these features:

  • Firestore - for storing birth profiles and cached responses
  • Functions - for server-side Vedika API calls
  • Hosting - for deploying your React app

When asked for the public directory, enter build (React's default output). Select "Yes" for single-page app rewriting.

Firebase Config

Create a Firebase config file. Get these values from the Firebase Console under Project Settings:

// src/firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getFunctions, httpsCallable } from 'firebase/functions';

const firebaseConfig = {
  apiKey: "YOUR_FIREBASE_API_KEY",
  authDomain: "your-project.firebaseapp.com",
  projectId: "your-project-id",
  storageBucket: "your-project.appspot.com",
  messagingSenderId: "123456789",
  appId: "1:123456789:web:abc123"
};

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const functions = getFunctions(app);
export { httpsCallable };

2 Firebase Auth Integration

Enable Email/Password authentication in the Firebase Console (Authentication > Sign-in method). Then create a signup/login component:

// src/components/Auth.js
import { useState } from 'react';
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword
} from 'firebase/auth';
import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
import { auth, db } from '../firebase';

export default function Auth() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSignUp, setIsSignUp] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');

    try {
      if (isSignUp) {
        // Create account
        const { user } = await createUserWithEmailAndPassword(
          auth, email, password
        );

        // Create user document in Firestore
        await setDoc(doc(db, 'users', user.uid), {
          email: user.email,
          createdAt: serverTimestamp(),
          profileCount: 0
        });
      } else {
        // Sign in
        await signInWithEmailAndPassword(auth, email, password);
      }
    } catch (err) {
      setError(err.message);
    }
  };

  return (
    <div className="auth-container">
      <h2>{isSignUp ? 'Create Account' : 'Sign In'}</h2>
      {error && <p className="error">{error}</p>}
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="Password (min 6 characters)"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          minLength={6}
        />
        <button type="submit">
          {isSignUp ? 'Sign Up' : 'Sign In'}
        </button>
      </form>
      <p>
        {isSignUp ? 'Already have an account?' : "Don't have an account?"}
        <button onClick={() => setIsSignUp(!isSignUp)}>
          {isSignUp ? 'Sign In' : 'Sign Up'}
        </button>
      </p>
    </div>
  );
}

Set up an auth state listener in your App component:

// src/App.js
import { useState, useEffect } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from './firebase';
import Auth from './components/Auth';
import Dashboard from './components/Dashboard';

function App() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
    return () => unsubscribe();
  }, []);

  if (loading) return <div>Loading...</div>;

  return user ? <Dashboard user={user} /> : <Auth />;
}

export default App;

3 Birth Profile Form

Astrology calculations require four pieces of data: date of birth, time of birth, place of birth (latitude/longitude), and timezone. Here is a React component that collects all four:

// src/components/BirthProfileForm.js
import { useState } from 'react';
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
import { db, auth } from '../firebase';

export default function BirthProfileForm({ onProfileSaved }) {
  const [name, setName] = useState('Self');
  const [birthDate, setBirthDate] = useState('');
  const [birthTime, setBirthTime] = useState('');
  const [unknownTime, setUnknownTime] = useState(false);
  const [place, setPlace] = useState('');
  const [latitude, setLatitude] = useState('');
  const [longitude, setLongitude] = useState('');
  const [saving, setSaving] = useState(false);

  // Resolve timezone offset from coordinates
  // Uses the Intl API to get the local UTC offset
  const getTimezoneOffset = (lat, lng, dateStr) => {
    try {
      // Create a date object at the birth location
      const date = new Date(dateStr + 'T12:00:00Z');
      // For production, use a timezone lookup library like geo-tz
      // This simplified version uses browser Intl API
      const offsetMinutes = date.getTimezoneOffset();
      const absMinutes = Math.abs(offsetMinutes);
      const hours = String(Math.floor(absMinutes / 60)).padStart(2, '0');
      const mins = String(absMinutes % 60).padStart(2, '0');
      const sign = offsetMinutes <= 0 ? '+' : '-';
      return `${sign}${hours}:${mins}`;
    } catch {
      return '+00:00';
    }
  };

  // Google Places Autocomplete integration
  // In production, use @react-google-maps/api or similar
  const handlePlaceSearch = async () => {
    if (!place.trim()) return;

    // Using browser Geocoding API as fallback
    // For production, use Google Places API with your API key:
    //
    // const response = await fetch(
    //   `https://maps.googleapis.com/maps/api/geocode/json` +
    //   `?address=${encodeURIComponent(place)}&key=YOUR_GOOGLE_KEY`
    // );
    // const data = await response.json();
    // const { lat, lng } = data.results[0].geometry.location;

    // Demo coordinates for common cities
    const cities = {
      'mumbai': { lat: 19.076, lng: 72.877 },
      'delhi': { lat: 28.614, lng: 77.209 },
      'bangalore': { lat: 12.972, lng: 77.594 },
      'chennai': { lat: 13.083, lng: 80.270 },
      'kolkata': { lat: 22.573, lng: 88.364 },
      'new york': { lat: 40.713, lng: -74.006 },
      'london': { lat: 51.507, lng: -0.128 },
    };

    const key = place.toLowerCase().trim();
    const match = Object.keys(cities).find(c => key.includes(c));
    if (match) {
      setLatitude(String(cities[match].lat));
      setLongitude(String(cities[match].lng));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!latitude || !longitude) {
      alert('Please enter or search for a birth place to get coordinates.');
      return;
    }

    setSaving(true);

    const time = unknownTime ? '12:00' : birthTime;
    const datetime = `${birthDate}T${time}:00`;
    const lat = parseFloat(latitude);
    const lng = parseFloat(longitude);

    // Resolve timezone - Vedika V1 expects +HH:MM format
    const timezone = getTimezoneOffset(lat, lng, birthDate);

    try {
      const profileRef = await addDoc(
        collection(db, 'users', auth.currentUser.uid, 'profiles'),
        {
          name,
          datetime,
          latitude: lat,
          longitude: lng,
          timezone,
          unknownTime,
          place,
          createdAt: serverTimestamp()
        }
      );

      onProfileSaved({
        id: profileRef.id,
        name,
        datetime,
        latitude: lat,
        longitude: lng,
        timezone,
        unknownTime
      });
    } catch (err) {
      console.error('Error saving profile:', err);
      alert('Failed to save profile. Please try again.');
    } finally {
      setSaving(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="birth-form">
      <h3>Add Birth Profile</h3>

      <label>
        Profile Name
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder='e.g., "Self", "Mom", "Partner"'
          required
        />
      </label>

      <label>
        Date of Birth
        <input
          type="date"
          value={birthDate}
          onChange={(e) => setBirthDate(e.target.value)}
          max={new Date().toISOString().split('T')[0]}
          required
        />
      </label>

      <label>
        Time of Birth
        <input
          type="time"
          value={birthTime}
          onChange={(e) => setBirthTime(e.target.value)}
          disabled={unknownTime}
          required={!unknownTime}
        />
      </label>

      <label className="checkbox-label">
        <input
          type="checkbox"
          checked={unknownTime}
          onChange={(e) => setUnknownTime(e.target.checked)}
        />
        I don't know the exact birth time (noon will be used)
      </label>

      <label>
        Place of Birth
        <div className="place-input">
          <input
            type="text"
            value={place}
            onChange={(e) => setPlace(e.target.value)}
            placeholder="City name, e.g., Mumbai"
          />
          <button type="button" onClick={handlePlaceSearch}>
            Search
          </button>
        </div>
      </label>

      <div className="coordinates">
        <label>
          Latitude
          <input
            type="number"
            step="0.001"
            value={latitude}
            onChange={(e) => setLatitude(e.target.value)}
            placeholder="19.076"
            required
          />
        </label>
        <label>
          Longitude
          <input
            type="number"
            step="0.001"
            value={longitude}
            onChange={(e) => setLongitude(e.target.value)}
            placeholder="72.877"
            required
          />
        </label>
      </div>

      <button type="submit" disabled={saving}>
        {saving ? 'Saving...' : 'Save Profile'}
      </button>
    </form>
  );
}

Production tip: For a real place-of-birth lookup, integrate Google Places Autocomplete. It returns latitude, longitude, and formatted address in one step. The demo above uses hardcoded cities for simplicity. Google Places also resolves timezone via the Time Zone API.

4 Storing Profiles in Firestore

Here is the Firestore collection structure. Each user gets their own subcollection of birth profiles:

// Firestore data model
//
// users/{uid}
//   - email: "user@example.com"
//   - createdAt: Timestamp
//   - profileCount: 3
//
// users/{uid}/profiles/{profileId}
//   - name: "Self"
//   - datetime: "1990-06-15T14:30:00"
//   - latitude: 19.076
//   - longitude: 72.877
//   - timezone: "+05:30"
//   - unknownTime: false
//   - place: "Mumbai, India"
//   - createdAt: Timestamp
//
// users/{uid}/charts/{chartHash}
//   - data: { ... Vedika API response }
//   - endpoint: "birth-chart"
//   - cachedAt: Timestamp
//   - expiresAt: Timestamp (24 hours from cachedAt)

Write Firestore security rules to ensure users can only access their own data:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Users can only read/write their own document
    match /users/{userId} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;

      // Birth profiles subcollection
      match /profiles/{profileId} {
        allow read, write: if request.auth != null
                           && request.auth.uid == userId;
      }

      // Cached chart responses
      match /charts/{chartId} {
        allow read, write: if request.auth != null
                           && request.auth.uid == userId;
      }
    }
  }
}

Deploy rules with firebase deploy --only firestore:rules.

Create a component that lists saved profiles and lets the user select one for chart generation:

// src/components/ProfileList.js
import { useState, useEffect } from 'react';
import { collection, query, orderBy, onSnapshot } from 'firebase/firestore';
import { db, auth } from '../firebase';

export default function ProfileList({ onSelectProfile }) {
  const [profiles, setProfiles] = useState([]);

  useEffect(() => {
    const q = query(
      collection(db, 'users', auth.currentUser.uid, 'profiles'),
      orderBy('createdAt', 'desc')
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const data = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));
      setProfiles(data);
    });

    return () => unsubscribe();
  }, []);

  return (
    <div className="profile-list">
      <h3>Your Birth Profiles</h3>
      {profiles.length === 0 ? (
        <p>No profiles saved yet. Add one above.</p>
      ) : (
        profiles.map(profile => (
          <div
            key={profile.id}
            className="profile-card"
            onClick={() => onSelectProfile(profile)}
          >
            <strong>{profile.name}</strong>
            <span>{profile.datetime}</span>
            <span>{profile.place}</span>
          </div>
        ))
      )}
    </div>
  );
}

5 Cloud Function to Call Vedika API

This is the most important architectural decision in the entire app: never call Vedika API from the browser. Your API key would be visible in the network tab. Cloud Functions run server-side, so the key stays secret.

Set your Vedika API key as a Cloud Functions environment variable:

# Set the API key as a secret (recommended for production)
firebase functions:secrets:set VEDIKA_API_KEY

# When prompted, paste your key: vk_live_abc123...

# Or use config for simpler setups
firebase functions:config:set vedika.api_key="vk_live_abc123..."

Create two Cloud Functions: one for birth chart data (V2 endpoint) and one for AI chat (V1 endpoint):

// functions/index.js
const { onCall, HttpsError } = require('firebase-functions/v2/https');
const { defineSecret } = require('firebase-functions/params');

const VEDIKA_API_KEY = defineSecret('VEDIKA_API_KEY');

// Birth Chart - calls Vedika V2 endpoint
exports.getBirthChart = onCall(
  { secrets: [VEDIKA_API_KEY] },
  async (request) => {
    // Verify the user is authenticated
    if (!request.auth) {
      throw new HttpsError(
        'unauthenticated',
        'You must be signed in to generate a birth chart.'
      );
    }

    const { datetime, latitude, longitude } = request.data;

    // Validate inputs
    if (!datetime || latitude === undefined || longitude === undefined) {
      throw new HttpsError(
        'invalid-argument',
        'datetime, latitude, and longitude are required.'
      );
    }

    try {
      const response = await fetch(
        'https://api.vedika.io/v2/astrology/birth-chart',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': VEDIKA_API_KEY.value()
          },
          body: JSON.stringify({
            datetime,
            latitude,
            longitude,
            ayanamsa: 'lahiri'
          })
        }
      );

      if (!response.ok) {
        const errorBody = await response.text();
        console.error('Vedika API error:', response.status, errorBody);
        throw new HttpsError(
          'internal',
          'Birth chart calculation failed. Please try again.'
        );
      }

      const data = await response.json();
      return data;
    } catch (error) {
      if (error instanceof HttpsError) throw error;
      console.error('Unexpected error:', error);
      throw new HttpsError('internal', 'Something went wrong.');
    }
  }
);

// AI Chat - calls Vedika V1 endpoint
exports.askVedika = onCall(
  { secrets: [VEDIKA_API_KEY] },
  async (request) => {
    if (!request.auth) {
      throw new HttpsError('unauthenticated', 'Sign in required.');
    }

    const { question, datetime, latitude, longitude, timezone } =
      request.data;

    if (!question) {
      throw new HttpsError(
        'invalid-argument',
        'A question is required.'
      );
    }

    try {
      const body = { question };

      // Add birth details if provided for personalized answers
      if (datetime && latitude !== undefined && longitude !== undefined) {
        body.birthDetails = {
          datetime,
          latitude,
          longitude,
          // V1 expects UTC offset format: "+05:30", not "Asia/Kolkata"
          timezone: timezone || '+05:30'
        };
      }

      const response = await fetch(
        'https://api.vedika.io/api/vedika/chat',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-api-key': VEDIKA_API_KEY.value()
          },
          body: JSON.stringify(body)
        }
      );

      if (!response.ok) {
        const errorBody = await response.text();
        console.error('Vedika chat error:', response.status, errorBody);
        throw new HttpsError('internal', 'AI chat request failed.');
      }

      const data = await response.json();
      return {
        answer: data.answer,
        followUpSuggestions: data.followUpSuggestions || []
      };
    } catch (error) {
      if (error instanceof HttpsError) throw error;
      console.error('Chat error:', error);
      throw new HttpsError('internal', 'Something went wrong.');
    }
  }
);

Install dependencies and deploy:

cd functions
npm install firebase-functions firebase-admin
cd ..
firebase deploy --only functions

Security note: The request.auth check is mandatory. Without it, anyone with your Cloud Functions URL can call Vedika API using your key. Every callable function must verify authentication before doing work.

6 Displaying Birth Chart Data

Call the Cloud Function from React and render the results. The V2 birth-chart endpoint returns planet positions, houses, nakshatras, and more:

// src/components/BirthChart.js
import { useState } from 'react';
import { functions, httpsCallable } from '../firebase';

const getBirthChart = httpsCallable(functions, 'getBirthChart');

export default function BirthChart({ profile }) {
  const [chartData, setChartData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const generateChart = async () => {
    setLoading(true);
    setError('');

    try {
      const result = await getBirthChart({
        datetime: profile.datetime,
        latitude: profile.latitude,
        longitude: profile.longitude
      });

      setChartData(result.data);
    } catch (err) {
      setError(err.message || 'Failed to generate chart');
    } finally {
      setLoading(false);
    }
  };

  if (!chartData) {
    return (
      <div className="chart-prompt">
        <h3>Birth Chart for {profile.name}</h3>
        <p>{profile.datetime} | {profile.place}</p>
        <button onClick={generateChart} disabled={loading}>
          {loading ? 'Calculating...' : 'Generate Birth Chart'}
        </button>
        {error && <p className="error">{error}</p>}
      </div>
    );
  }

  return (
    <div className="birth-chart">
      <h3>Birth Chart: {profile.name}</h3>

      {/* Ascendant */}
      <div className="chart-section">
        <h4>Ascendant (Lagna)</h4>
        <p className="highlight">
          {chartData.ascendant?.sign} at{' '}
          {chartData.ascendant?.degree?.toFixed(2)}°
        </p>
      </div>

      {/* Planet Positions Table */}
      <div className="chart-section">
        <h4>Planetary Positions</h4>
        <table>
          <thead>
            <tr>
              <th>Planet</th>
              <th>Sign</th>
              <th>Degree</th>
              <th>Nakshatra</th>
              <th>House</th>
              <th>Retro</th>
            </tr>
          </thead>
          <tbody>
            {chartData.planets?.map((planet) => (
              <tr key={planet.name}>
                <td>{planet.name}</td>
                <td>{planet.sign}</td>
                <td>{planet.signDegree?.toFixed(2)}°</td>
                <td>{planet.nakshatra} (Pada {planet.nakshatraPada})</td>
                <td>{planet.house}</td>
                <td>{planet.retrograde ? 'R' : '-'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Moon Sign and Nakshatra */}
      {chartData.planets && (
        <div className="chart-section">
          <h4>Key Details</h4>
          <ul>
            <li>
              <strong>Moon Sign (Rashi):</strong>{' '}
              {chartData.planets.find(p => p.name === 'Moon')?.sign}
            </li>
            <li>
              <strong>Moon Nakshatra:</strong>{' '}
              {chartData.planets.find(p => p.name === 'Moon')?.nakshatra}
            </li>
            <li>
              <strong>Sun Sign:</strong>{' '}
              {chartData.planets.find(p => p.name === 'Sun')?.sign}
            </li>
          </ul>
        </div>
      )}

      {/* Dasha Period */}
      {chartData.dasha && (
        <div className="chart-section">
          <h4>Current Dasha Period</h4>
          <p>
            <strong>Mahadasha:</strong> {chartData.dasha.currentMahadasha}
          </p>
          <p>
            <strong>Antardasha:</strong> {chartData.dasha.currentAntardasha}
          </p>
        </div>
      )}
    </div>
  );
}

7 Adding AI Chat

Vedika AI lets users ask natural language questions about their birth chart. The AI analyzes their planetary positions and returns personalized interpretations. Build a chat interface that sends questions through your Cloud Function:

// src/components/AstroChat.js
import { useState, useRef, useEffect } from 'react';
import { functions, httpsCallable } from '../firebase';

const askVedika = httpsCallable(functions, 'askVedika');

export default function AstroChat({ profile }) {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);
  const messagesEndRef = useRef(null);

  // Suggested questions to help users get started
  const suggestions = [
    'What does my birth chart say about my career?',
    'When is a good time for marriage based on my chart?',
    'What is my current Mahadasha telling me?',
    'Am I going through Sade Sati?',
    'What yogas are present in my birth chart?'
  ];

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const sendMessage = async (text) => {
    const question = text || input;
    if (!question.trim() || loading) return;

    setInput('');
    setMessages(prev => [
      ...prev,
      { role: 'user', text: question }
    ]);
    setLoading(true);

    try {
      const result = await askVedika({
        question,
        datetime: profile.datetime,
        latitude: profile.latitude,
        longitude: profile.longitude,
        timezone: profile.timezone
      });

      setMessages(prev => [
        ...prev,
        {
          role: 'assistant',
          text: result.data.answer,
          suggestions: result.data.followUpSuggestions
        }
      ]);
    } catch (err) {
      setMessages(prev => [
        ...prev,
        { role: 'assistant', text: 'Sorry, something went wrong. Try again.' }
      ]);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="astro-chat">
      <div className="chat-header">
        <h3>Ask Vedika AI</h3>
        <p>Personalized for {profile.name}'s birth chart</p>
      </div>

      <div className="chat-messages">
        {messages.length === 0 && (
          <div className="suggestions">
            <p>Try asking:</p>
            {suggestions.map((s, i) => (
              <button key={i} onClick={() => sendMessage(s)}>
                {s}
              </button>
            ))}
          </div>
        )}

        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            <div className="message-text">{msg.text}</div>
            {msg.suggestions?.length > 0 && (
              <div className="follow-ups">
                {msg.suggestions.map((s, j) => (
                  <button key={j} onClick={() => sendMessage(s)}>
                    {s}
                  </button>
                ))}
              </div>
            )}
          </div>
        ))}

        {loading && (
          <div className="message assistant">
            <div className="message-text typing">
              Analyzing your chart...
            </div>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      <form
        className="chat-input"
        onSubmit={(e) => { e.preventDefault(); sendMessage(); }}
      >
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask about career, love, health..."
          disabled={loading}
        />
        <button type="submit" disabled={loading || !input.trim()}>
          Send
        </button>
      </form>
    </div>
  );
}

8 Caching API Responses in Firestore

Birth chart data for a given datetime and location does not change. Calling the API twice with the same inputs wastes money. Cache the response in Firestore and check the cache before making API calls:

// src/utils/chartCache.js
import {
  doc, getDoc, setDoc, Timestamp
} from 'firebase/firestore';
import { db, auth, functions, httpsCallable } from '../firebase';
import crypto from 'crypto-js';

const getBirthChart = httpsCallable(functions, 'getBirthChart');

// Generate a deterministic hash from birth details
function createChartHash(datetime, latitude, longitude) {
  const key = `${datetime}|${latitude.toFixed(3)}|${longitude.toFixed(3)}`;
  // Simple hash for cache key
  let hash = 0;
  for (let i = 0; i < key.length; i++) {
    const char = key.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash |= 0; // Convert to 32-bit integer
  }
  return 'chart_' + Math.abs(hash).toString(36);
}

export async function getCachedBirthChart(profile) {
  const uid = auth.currentUser.uid;
  const chartHash = createChartHash(
    profile.datetime,
    profile.latitude,
    profile.longitude
  );

  const cacheRef = doc(db, 'users', uid, 'charts', chartHash);

  // Check cache first
  const cached = await getDoc(cacheRef);
  if (cached.exists()) {
    const cacheData = cached.data();
    const now = Timestamp.now();

    // Return cached data if not expired (24-hour TTL)
    if (cacheData.expiresAt && cacheData.expiresAt > now) {
      console.log('Cache hit - returning stored chart data');
      return cacheData.data;
    }
  }

  // Cache miss - call Vedika API via Cloud Function
  console.log('Cache miss - calling Vedika API');
  const result = await getBirthChart({
    datetime: profile.datetime,
    latitude: profile.latitude,
    longitude: profile.longitude
  });

  // Store in cache with 24-hour expiry
  const now = Timestamp.now();
  const expiresAt = new Timestamp(
    now.seconds + 86400, // 24 hours
    now.nanoseconds
  );

  await setDoc(cacheRef, {
    data: result.data,
    endpoint: 'birth-chart',
    cachedAt: now,
    expiresAt
  });

  return result.data;
}

Update the BirthChart component to use the cache:

// In BirthChart.js, replace the direct Cloud Function call:

// Before (no cache):
// const result = await getBirthChart({ ... });
// setChartData(result.data);

// After (with cache):
import { getCachedBirthChart } from '../utils/chartCache';

const generateChart = async () => {
  setLoading(true);
  setError('');

  try {
    const data = await getCachedBirthChart(profile);
    setChartData(data);
  } catch (err) {
    setError(err.message || 'Failed to generate chart');
  } finally {
    setLoading(false);
  }
};

Cost impact: A user who views their birth chart 10 times in a day triggers 1 API call and 9 cache reads. At Firestore's free tier (50K reads/day), this caching pattern alone can reduce your Vedika API costs by 80-90% for active users.

9 Dashboard: Putting It All Together

Wire all the components into a Dashboard that shows after login:

// src/components/Dashboard.js
import { useState } from 'react';
import { signOut } from 'firebase/auth';
import { auth } from '../firebase';
import BirthProfileForm from './BirthProfileForm';
import ProfileList from './ProfileList';
import BirthChart from './BirthChart';
import AstroChat from './AstroChat';

export default function Dashboard({ user }) {
  const [selectedProfile, setSelectedProfile] = useState(null);
  const [activeTab, setActiveTab] = useState('chart');

  return (
    <div className="dashboard">
      <header>
        <h1>Astro App</h1>
        <div>
          <span>{user.email}</span>
          <button onClick={() => signOut(auth)}>Sign Out</button>
        </div>
      </header>

      <div className="dashboard-layout">
        {/* Sidebar: profiles */}
        <aside>
          <BirthProfileForm
            onProfileSaved={(profile) => setSelectedProfile(profile)}
          />
          <ProfileList
            onSelectProfile={(profile) => setSelectedProfile(profile)}
          />
        </aside>

        {/* Main content */}
        <main>
          {selectedProfile ? (
            <>
              <nav className="tabs">
                <button
                  className={activeTab === 'chart' ? 'active' : ''}
                  onClick={() => setActiveTab('chart')}
                >
                  Birth Chart
                </button>
                <button
                  className={activeTab === 'chat' ? 'active' : ''}
                  onClick={() => setActiveTab('chat')}
                >
                  Ask AI
                </button>
              </nav>

              {activeTab === 'chart' && (
                <BirthChart profile={selectedProfile} />
              )}
              {activeTab === 'chat' && (
                <AstroChat profile={selectedProfile} />
              )}
            </>
          ) : (
            <div className="empty-state">
              <h2>Select or create a birth profile</h2>
              <p>Add a birth profile to generate charts
                and ask questions.</p>
            </div>
          )}
        </main>
      </div>
    </div>
  );
}

10 Deploy to Firebase Hosting

Build the React app and deploy everything in one command:

# Build React app
npm run build

# Deploy everything: Hosting + Functions + Firestore Rules
firebase deploy

# Or deploy individually:
firebase deploy --only hosting      # Just the frontend
firebase deploy --only functions    # Just Cloud Functions
firebase deploy --only firestore    # Just security rules

# Your app is now live at:
# https://your-project-id.web.app

Firebase Hosting provides a free SSL certificate and CDN distribution. Your app is served from the nearest edge location to each user.

11 Cost Breakdown: 1,000 MAU

Here is what this app costs to run with 1,000 monthly active users, assuming each user views their chart 3 times and asks 2 AI questions per month:

Service Usage Monthly Cost
Firebase Auth 1,000 users $0 (free tier: 10K/mo)
Firestore ~15K reads + ~3K writes $0 (free tier: 50K reads/day)
Cloud Functions ~3K invocations $0 (free tier: 125K/mo)
Firebase Hosting ~5 GB bandwidth $0 (free tier: 10 GB/mo)
Vedika API (Starter) ~3K API calls (after caching) $12/month
Total ~$12/month

The Firebase free tier (Spark plan) covers all infrastructure costs at this scale. Your only cost is the Vedika API subscription. The Starter plan at $12/month provides enough credits for approximately 5,000 API calls. With Firestore caching, 1,000 MAU generating 3,000 unique chart requests fits comfortably within that budget.

If you grow beyond 5,000 unique API calls per month, the Professional plan at $60/month covers higher volumes. Enterprise at $240/month includes the ability to top up wallet credits for unpredictable spikes.

12 Gotchas and Common Mistakes

Never put your Vedika API key in client-side code

Anyone can open browser DevTools, inspect network requests, and steal your key. Always route API calls through Cloud Functions. This is the single most common mistake in Firebase + API integrations.

V1 timezone format is +HH:MM, not IANA

Vedika V1 endpoints (like /api/vedika/chat) expect UTC offset format: +05:30, -08:00, +00:00. Sending Asia/Kolkata or America/New_York will cause a validation error. Convert IANA timezone names to UTC offsets before calling V1 endpoints.

Cache aggressively: birth chart data does not change

A birth chart for "June 15, 1990, 2:30 PM, Mumbai" returns the same planetary positions every time. There is no reason to call the API twice for the same inputs. Cache in Firestore with a 24-hour TTL (or longer). Without caching, you will burn through your API credits unnecessarily.

Firebase free tier limits

The Spark plan includes 50K Firestore reads/day, 20K writes/day, and 125K Cloud Function invocations/month. These limits are generous for apps under 5,000 MAU. Beyond that, switch to the Blaze plan (pay-as-you-go) where costs are still minimal at scale.

Use sandbox endpoints during development

Vedika provides free sandbox endpoints at vedika.io/sandbox with 65 mock endpoints that return realistic data. No API key needed. Build and test your entire frontend against the sandbox, then switch to production endpoints with your live key when you are ready to ship.

V1 request format: question, not query

The V1 chat endpoint expects { "question": "...", "birthDetails": {...} }. The field name is question, not query or prompt. Birth data goes inside a birthDetails object with datetime, latitude, longitude, and timezone fields.

API key format

Production API keys start with vk_live_. Enterprise keys start with vk_ent_. Test keys (vk_test_) are rejected by the API. Make sure you are using a live key in production.

"I don't know my birth time" handling

Many users do not know their exact birth time. Default to noon (12:00) and flag the profile as unknownTime: true. Display a disclaimer that ascendant, house placements, and Moon nakshatra may be inaccurate without exact birth time. The Sun sign, planetary signs, and general dasha periods are still valid.

Extending the App

Once the foundation is working, there are several features you can add:

Daily Horoscope Notifications

Use Firebase Cloud Messaging (FCM) to send daily horoscope notifications. Schedule a Cloud Function with Cloud Scheduler to run at midnight IST, fetch predictions for each user's Moon sign, and push the notification:

// functions/scheduled.js
const { onSchedule } = require('firebase-functions/v2/scheduler');
const { getFirestore } = require('firebase-admin/firestore');
const admin = require('firebase-admin');
admin.initializeApp();

exports.dailyHoroscope = onSchedule(
  { schedule: '0 0 * * *', timeZone: 'Asia/Kolkata' },
  async () => {
    const db = getFirestore();
    const users = await db.collection('users')
      .where('notificationsEnabled', '==', true)
      .get();

    for (const userDoc of users.docs) {
      const moonSign = userDoc.data().moonSign;
      if (!moonSign) continue;

      // Send FCM notification
      const token = userDoc.data().fcmToken;
      if (token) {
        await admin.messaging().send({
          token,
          notification: {
            title: `Daily ${moonSign} Horoscope`,
            body: 'Your personalized predictions are ready.'
          }
        });
      }
    }
  }
);

Multi-Language Support

Vedika AI supports 30+ languages. Pass the question in any language and the AI responds in that language. Users asking in Hindi get Hindi responses:

// Hindi question - response comes back in Hindi
const result = await askVedika({
  question: 'मेरी कुंडली में कौन से योग हैं?',
  datetime: profile.datetime,
  latitude: profile.latitude,
  longitude: profile.longitude,
  timezone: profile.timezone
});

// Tamil question - response comes back in Tamil
const result = await askVedika({
  question: 'என் ஜாதகத்தில் என்ன யோகங்கள் உள்ளன?',
  datetime: profile.datetime,
  latitude: profile.latitude,
  longitude: profile.longitude,
  timezone: profile.timezone
});

Compatibility Matching

Allow users to save multiple birth profiles and compare them for compatibility. Ask Vedika AI to analyze two charts together:

const result = await askVedika({
  question: `Compare compatibility between person born on ` +
    `${profile1.datetime} at ${profile1.place} and person ` +
    `born on ${profile2.datetime} at ${profile2.place}. ` +
    `Analyze Guna Milan and key planetary aspects.`,
  datetime: profile1.datetime,
  latitude: profile1.latitude,
  longitude: profile1.longitude,
  timezone: profile1.timezone
});

Vedika V2 Endpoints

Beyond birth charts, Vedika API offers 120+ endpoints. Add more Cloud Functions for specific calculations:

Endpoint What It Returns Use Case
/v2/astrology/birth-chartFull chart with planets, houses, nakshatrasCore chart display
/v2/astrology/planetsPlanetary positions with degreesChart visualization
/v2/astrology/dashaMahadasha + antardasha periodsTimeline predictions
/v2/astrology/panchangDaily panchang (tithi, nakshatra, yoga)Calendar features
/v2/astrology/kundli-matchGuna Milan compatibility scoreMatchmaking
/api/vedika/chatAI interpretation of any questionChatbot

See the full list of 120+ endpoints in the Vedika API documentation.

Final Project Structure

astro-app/
  src/
    firebase.js              # Firebase config + exports
    App.js                   # Auth state router
    components/
      Auth.js                # Login / signup form
      Dashboard.js           # Main layout after login
      BirthProfileForm.js    # Birth data collection form
      ProfileList.js         # List of saved profiles
      BirthChart.js          # Chart display component
      AstroChat.js           # AI chat interface
    utils/
      chartCache.js          # Firestore caching layer
  functions/
    index.js                 # getBirthChart + askVedika
    package.json
  firestore.rules            # Security rules
  firebase.json              # Firebase project config
  package.json

Conclusion

This tutorial covered the full stack: user authentication, data persistence, server-side API security, birth chart calculations with Swiss Ephemeris precision, AI-powered interpretations, response caching, and deployment. The key architectural decisions:

  • Cloud Functions for API calls keeps your Vedika API key out of browser code
  • Firestore caching prevents duplicate API charges for the same chart
  • Firebase Auth + Firestore rules ensure users can only access their own data
  • Vedika AI chat eliminates the need to build your own astrological interpretation engine
  • Firebase Hosting gives you SSL, CDN, and custom domain support out of the box

The total cost for a 1,000 MAU app is approximately $12/month: entirely from the Vedika API Starter plan, with Firebase infrastructure on the free tier. That is a production-ready astrology application for less than the cost of a single lunch.

To go further: add push notifications for daily horoscopes, implement compatibility matching between multiple profiles, visualize charts with a canvas library like Chart.js or D3, and support multiple Indian languages. Vedika API handles the astrology. Firebase handles the infrastructure. You handle the product.

Start Building

Vedika API provides 120+ Vedic and Western astrology endpoints with Swiss Ephemeris precision, plus AI-powered natural language interpretations in 30+ languages. Use the free sandbox to build and test, then go live with a $12/month Starter plan.


About Vedika Intelligence: Vedika is a B2B astrology API with AI-powered chatbot capabilities, 120+ calculation endpoints, Swiss Ephemeris precision, and support for 30+ languages. Production apps use Vedika for birth charts, daily horoscopes, compatibility matching, dasha predictions, and natural language astrology Q&A.