Note on this walkthrough. This is an aspirational technical blueprint, not a published customer case. It is the architecture we recommend when a developer messages support asking "how would I build this?" The patterns are validated by our own usage logs across many mobile-app customers, but the numbers and code below are illustrative — your real-world costs and latencies will depend on your traffic shape, geography, and platform choices.
You're building a daily personalized horoscope app. Users sign up with birth data, get one tailored daily reading delivered as a push notification at their local morning time, and can tap in for "what's happening this week" and "year ahead" details. Target: 10,000 daily active users in year one, sustainable on a single founder's budget.
Here's the full blueprint.
Architecture in plain words
Three moving parts:
- Mobile client — React Native, Swift, or Kotlin. Renders the horoscope, manages the sign-up flow, handles push notifications, caches data locally.
- Your backend — one small server (Vercel, Railway, Fly.io, or Cloudflare Workers). Stores user accounts, holds API keys, runs the daily cron job that pre-generates horoscopes.
- Astrology API — our endpoints. You never call us directly from the mobile client (that would leak your API key); you call us from your backend.
Data flow:
1User opens app for first time2 → Mobile client collects birth date, time, place3 → POSTs to your backend (signup endpoint)4 → Backend calls /p/natal-api once, stores result in your DB5 → User is now "set up"6
7Every day at 03:00 UTC8 → Backend cron job runs9 → For each active user: backend calls /p/personalized-horoscopes-api10 → Result stored in DB keyed by user_id + date11 → Backend sends push notification at user's local 08:0012
13User taps notification14 → Mobile client fetches today's horoscope from your DB (already cached)15 → Renders it instantlyThe endpoint sequence
Step 1: Sign-up (one-time per user)
1// On your backend, called when user submits birth data2async function onUserSignup(userId: string, birthData: BirthData) {3 const natal = await fetch('https://api.astrology-api.io/v1/natal', {4 method: 'POST',5 headers: { 'Authorization': `Bearer ${process.env.ASTROLOGY_API_KEY}` },6 body: JSON.stringify({7 date: birthData.date, // e.g. "1990-04-15"8 time: birthData.time, // e.g. "14:30"9 latitude: birthData.lat, // e.g. 40.712810 longitude: birthData.lon, // e.g. -74.006011 timezone: birthData.tz, // e.g. "America/New_York"12 }),13 }).then(r => r.json());14
15 await db.users.update(userId, {16 natalChart: natal,17 natalCachedAt: new Date(),18 });19}This call runs once per user, ever. The chart never changes — your natal placements at birth are fixed.
Step 2: Daily cron (once per user per day)
1// Backend cron, runs at 03:00 UTC every day2async function generateDailyHoroscopes() {3 const activeUsers = await db.users.findActive();4
5 // Bulk-batch in groups of 100 to use the bulk endpoint6 for (const batch of chunk(activeUsers, 100)) {7 const responses = await fetch('https://api.astrology-api.io/v1/personalized-horoscopes/bulk', {8 method: 'POST',9 headers: { 'Authorization': `Bearer ${process.env.ASTROLOGY_API_KEY}` },10 body: JSON.stringify({11 charts: batch.map(u => ({ userId: u.id, natalChart: u.natalChart })),12 date: new Date().toISOString().slice(0, 10),13 language: 'en',14 }),15 }).then(r => r.json());16
17 for (const horoscope of responses.results) {18 await db.dailyHoroscopes.upsert({19 userId: horoscope.userId,20 date: horoscope.date,21 content: horoscope.content,22 });23 }24 }25}100 charts in one request reduces network overhead. At 10,000 active users that's 100 API calls per day total, not 10,000.
Step 3: Push notification
1// Backend, runs continuously, watches for users whose local 08:00 has arrived2async function dispatchMorningNotifications() {3 const users = await db.users.findReadyForMorningPush();4 for (const user of users) {5 const today = await db.dailyHoroscopes.findOne({ userId: user.id, date: today() });6 await pushService.send(user.deviceToken, {7 title: 'Your daily horoscope',8 body: today.content.headline,9 data: { horoscopeId: today.id },10 });11 }12}Step 4: Mobile client fetches from your DB
1// Swift, iOS client2func fetchTodayHoroscope() async throws -> Horoscope {3 let url = URL(string: "https://your-backend.com/api/horoscope/today")!4 var request = URLRequest(url: url)5 request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")6 let (data, _) = try await URLSession.shared.data(for: request)7 return try JSONDecoder().decode(Horoscope.self, from: data)8}1// Kotlin, Android client2suspend fun fetchTodayHoroscope(): Horoscope {3 val response = httpClient.get("https://your-backend.com/api/horoscope/today") {4 headers { append("Authorization", "Bearer $authToken") }5 }6 return response.body()7}The mobile client never touches the astrology API directly. It only ever talks to your backend, which serves pre-computed daily content from your database.
Cost math at 10,000 DAU
Assumptions: 10,000 daily active users, 30 days/month, daily horoscope per user, plus occasional on-demand transit lookups (let's say 20% of users tap the "what's happening this week" tab on any given day).
- Sign-up calls (natal): negligible after onboarding ramp. Say 500 new users a month = 500 calls.
- Daily horoscopes (bulk, 100 per request): 10,000 users × 30 days ÷ 100 batch size = 3,000 requests.
- On-demand transits: 10,000 × 20% × 30 days = 60,000 individual requests. With aggressive caching on your backend (6-hour TTL), realistic API calls drop to about 10,000.
That fits inside our Professional tier ($37/month, 55,000 requests). You have headroom for growth to roughly 35,000 DAU before the Business tier ($99/month, 220,000 requests) becomes necessary.
Latency budget
End-to-end latency the user sees, when they tap the morning notification:
| Step | Time |
|---|---|
| Notification tap → app open | Platform-dependent (~200ms iOS, ~300ms Android) |
| App → your backend (HTTPS round trip) | 50-150ms |
| Your backend → DB read | 5-20ms |
| Backend → app response | 50-150ms |
| Render | 16-50ms |
| Total user-perceived latency | ~400-600ms |
What can go wrong (and how to handle it)
- The daily cron job fails partway through. Wrap each batch in a try/catch, log failures with user IDs, and re-run failed batches in a separate retry job. Bulk endpoint failures are usually transient.
- A user's birth time is missing. The personalized horoscope endpoint accepts unknown time inputs and returns what's possible without it. Display a UI hint asking them to add birth time later for more accuracy.
- A user travels across time zones. Decide once: does "morning" mean local time at registration or current location? We recommend current location — store a device-reported timezone on every app open and update the push scheduling accordingly.
- You hit the request limit mid-month. The dashboard sends an alert at 80% utilization. Upgrade tier before that hits and your service never goes down.
Internal links
- /p/natal-api — the foundation call you make once per user.
- /p/transit-api — current planetary movement for the "this week" tab.
- /p/personalized-horoscopes-api — the daily content endpoint with bulk batching.
- /pricing — tier breakdown and request limits.
Wrap-up
10,000 DAU on $37/month of API spend is a real, achievable target if you cache the natal chart once, batch the daily cron through the bulk endpoint, and never call the astrology API from the mobile client directly. The architecture is small enough to fit on a single Vercel project. Build the sign-up flow first, layer the cron job second, ship the notification layer last.
If you want to validate the request counts before writing code, the free tier (50 requests/month) is enough to run the sign-up flow for a handful of test users and see actual response shapes.


