The cost analysis behind the six tiers on /plans. Includes service-by-service unit costs, per-child economics, edge cases the naïve model misses, and what to instrument before billing ships.
01 · Today
Neni is multi-tenant by kindergartenId. Every tenant is currently on an implicit Free plan — there is no Plan, Subscription, or Invoice model in the Prisma schema, and there is no Stripe SDK in the workspace. Self-serve billing is scheduled for Phase 5 (Weeks 19+) per the roadmap.
Kindergarten.monthlyFeeCents is the kindergarten's fee charged to parents — NOT Neni's fee to the kindergarten. The maintainer analytics already treat monthlyFeeCents × childrenCount as the tenant's revenue. When billing lands, the price Neni charges must live on a new Subscription.priceCents field. Do not overload the existing column.02 · Cost basis
Ten paid services contribute to cost-of-goods today or in Phase 3 once worker AI lands. Items 6 and 7 (face recognition + speech-to-text) are currently mocked — they will dominate marginal cost the moment they go live.
| # | Service | Live today? | Unit cost (USD) | What drives it |
|---|---|---|---|---|
| 1 | S3 / MinIO storage | Yes | $0.023/GB-mo · $0.09/GB egress · $0.005/1k PUT | Photo & video size × retention |
| 2 | Supabase Postgres | Yes | $25–$599/mo + storage | Connection cap is the real ceiling |
| 3 | Redis ×2 (BullMQ + Upstash KV) | Yes | Upstash $0.20/100k req · ElastiCache ~$15–50/mo | 5 queues, OTP & refresh-token store |
| 4 | Vercel (API + 3 dashboards) | Yes | $20/seat + $0.60/1M invocations + $40/100GB egress | Function invocations |
| 5 | Vercel AI Gateway · GPT-4o-mini | Yes | $0.15/1M input · $0.60/1M output | Up to 6 tool steps per chat turn |
| 6 | Face recognition (Rekognition target) | Mocked | $0.001 per image search · $0.001 per index | 1–N calls per photo (per face) |
| 7 | Speech-to-text (Whisper / Azure target) | Mocked | Whisper $0.006/min · AWS Transcribe $0.024/min | Audio minutes per child |
| 8 | Resend (transactional email) | Yes | ~$0.0004 per email | OTPs, invites, password reset |
| 9 | Telegram Gateway (phone OTP) | Yes | $0.01–0.05 per OTP (returned by API) | Login events |
| 10 | FCM push | Stubbed | Free | TODO in worker; cost = $0 |
Scales with child count: storage, egress, face recognition (1–N per photo), STT, notifications (multiplied by parent-count).
Does NOT scale with child count: Vercel base, OTP volume (logins), Redis BullMQ, Supabase connection cap (it's a ceiling, not a meter).
03 · Unit economics
The model is sensitive to a handful of inputs. The team needs to validate these against real pilot data — anything off by 2× swings the answer.
| Assumption | Conservative | Realistic | Aggressive |
|---|---|---|---|
| Photos / child / day uploaded | 2 | 5 | 10 |
| Average photo size (compressed) | 0.6 MB | 1.0 MB | 1.5 MB |
| Lifetime parent views per photo | 3 | 5 | 10 |
| Retention (locked) | 6 mo | 6 mo | 6 mo |
| Parents per child | 1.5 | 1.8 | 2.5 |
| Push notifications / child / day | 2 | 4 | 6 |
| Audio minutes / child / day (P3) | 0 | 1 | 5 |
| Faces detected per photo | 1 | 2 | 4 |
| Cost line | Phase 1 (today) | Phase 3 (face on) | Phase 3 full AI |
|---|---|---|---|
| Storage @ 6 mo retention | $0.02 | $0.02 | $0.02 |
| Egress (S3 → parents, no CDN) | $0.07 | $0.07 | $0.07 |
| Face recognition (2 faces × 5 photos × 30d) | — | $0.30 | $0.30 |
| Speech-to-text (1 min/day × Whisper) | — | — | $0.18 |
| Push (FCM, free) | $0 | $0 | $0 |
| Email (Resend) | $0.003 | $0.003 | $0.003 |
| Telegram OTP (amortized) | $0.01 | $0.01 | $0.01 |
| Database allocation | $0.01 | $0.01 | $0.02 |
| Redis allocation | $0.001 | $0.001 | $0.001 |
| Vercel allocation | $0.02 | $0.02 | $0.02 |
| LLM dashboard chat (staff-amortized) | $0.05 | $0.05 | $0.15 |
| Observability (PostHog) | $0.05 | $0.05 | $0.05 |
| Marginal / child / mo | ~$0.25 | ~$0.55 | ~$0.85 |
Plus a fixed cost per tenant (Vercel seat slice, Supabase share, monitoring base): $15–30/mo.
DATA_CLEANUP queue.04 · Cost to serve
Min = light usage (Phase 1, no AI, 2 photos/day). Max = heavy usage (Phase 3 with face + STT + LLM chat, 10 photos/day).
| Tier | Top of band | Min $/mo | Max $/mo |
|---|---|---|---|
| Starter | 49 | $20 | $180 |
| Growth | 99 | $25 | $332 |
| Scale | 199 | $40 | $643 |
| Pro | 249 | $45 | $800 |
| Chain | 499 | $75 | $1,497 |
| Network | 799 | $110 | $2,312 |
For a typical 199-child tenant on full Phase-3 AI, the cost composition is striking:
| Cost line | $/child/mo (heavy) | Why |
|---|---|---|
| Egress | $0.40 | No CDN — direct S3 reads |
| Face recognition | $1.20 | Rekognition per face per image |
| Speech-to-text | $0.90 | Optional Phase-3 feature |
| LLM chat | $0.30 | GPT-4o-mini, 6-step tool loop |
| Storage (6mo retention) | $0.06 | Halved by the retention decision |
| Everything else | $0.19 | Email, OTP, DB, Redis, Vercel, observability |
05 · Pricing
The min price is aggressive market-entry (~25-30% margin even on heavy users with AI add-on). The max price is premium / international list (~75-80% margin on typical usage). Recommendation: launch at the midpoint, watch margin data for 90 days, adjust.
| Tier | Children | Min $/mo | Max $/mo | AI Pack add-on |
|---|---|---|---|---|
| Starter | 0–49 | $39 | $99 | +$19/mo flat |
| Growth | 50–99 | $89 | $229 | +$39/mo flat |
| Scale | 100–199 | $169 | $399 | +$0.40/child/mo |
| Pro | 200–249 | $249 | $549 | +$0.35/child/mo |
| Chain | 250–499 | $429 | $899 | +$0.30/child/mo |
| Network | 500–799 | $649 | $1,399 | +$0.25/child/mo |
| 800+ | Custom | $1.00/child | $1.75/child | Negotiated |
| Tier | Children | Min UZS/mo | Max UZS/mo | AI Pack |
|---|---|---|---|---|
| Starter | 0–49 | 300,000 | 750,000 | +150,000 |
| Growth | 50–99 | 650,000 | 1,700,000 | +300,000 |
| Scale | 100–199 | 1,250,000 | 2,950,000 | +3,000/child |
| Pro | 200–249 | 1,850,000 | 4,100,000 | +2,500/child |
| Chain | 250–499 | 3,200,000 | 6,650,000 | +2,200/child |
| Network | 500–799 | 4,800,000 | 10,400,000 | +1,900/child |
UZS prices are discounted ~40% from USD to reflect Uzbek kindergarten ARR. A typical KG charging 1.5–3M UZS per parent per month gets to ~3-6% of revenue spent on tech — well inside the budget envelope.
06 · Edge cases
| Edge case | Risk | Mitigation |
|---|---|---|
| A child has 2–3 parents (ChildParentLink) | Push / email / Telegram scale with parent count | Meter parent events; cap free parents at 2, charge for extras |
| Video uploads | A 30s clip ≈ 30 MB ≈ 30 photos in storage + egress | Block on Starter; monthly video-GB allowance + overage on Growth+ |
| Archived children consume storage | 6-month retention helps but cohort data still piles up | Auto-archive to Glacier / Intelligent-Tiering after 60 days |
| MediaFile.sizeBytes is nullable | Storage billing is unauditable today | Backfill from S3 HeadObject in MediaProcessingJobService — blocking |
| Brand asset 7-day signed URLs | School logo re-fetched per page load | Move brand assets behind a CDN |
| Multi-branch tenants | One paying tenant with N × children | Count children across all branches under one Kindergarten |
| AI suggestions vs confirmations | Cost incurred even when staff rejects | Bill on attempts; surface rejection rate to help staff tune |
| Supabase 15-connection cap | Concurrency limit, not $; blocks Enterprise scale | Move off Session Pooler before Enterprise launch |
| Chat-happy tenant with LLM | GPT-4o-mini × 6 tool steps × big system prompt = bill spike | Per-tenant monthly token cap; consider Claude Haiku for cheaper input |
| No CDN | Egress is ~25% of marginal cost | CloudFront or R2 migration before Chain tier |
| Stripe doesn’t serve UZ retail well | Cannot collect from Uzbek customers | Integrate Click, Payme, Uzcard / Humo (3-4 weeks) |
| Uzbekistan VAT (12%) | Net vs gross pricing confusion | Show VAT-inclusive in UZS UI; track VAT line on invoice |
| Trial → conversion | Free-tier load with no revenue | 30-day Growth trial; auto-downgrade to Starter on trial end |
| GDPR data export | Egress spike on offboarding | Include in churn cost model; cap export size or charge |
07 · Schema
When Phase 5 lands, the schema needs four new models. Design now so the metering you start collecting today fits without rewrites.
model Plan {
id String @id @default(cuid())
code String @unique // starter | growth | scale | pro | chain | network | custom
displayName String
childMin Int
childMax Int? // null = unlimited
baseFeeCents Int
perChildOverageCents Int @default(0)
retentionDays Int // 180 for the 6-month policy
includesAi Boolean @default(false)
isPublic Boolean @default(true)
archivedAt DateTime?
}
model Subscription {
id String @id @default(cuid())
kindergartenId String @unique
planId String
currency String // USD | UZS
status SubscriptionStatus // TRIAL | ACTIVE | PAST_DUE | PAUSED | CANCELED
trialEndsAt DateTime?
currentPeriodStart DateTime
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
priceOverrideCents Int? // negotiated Enterprise deals
kindergarten Kindergarten @relation(fields: [kindergartenId], references: [id])
plan Plan @relation(fields: [planId], references: [id])
invoices Invoice[]
@@index([status])
}
model UsageRecord {
id String @id @default(cuid())
kindergartenId String
metric UsageMetric // CHILDREN_ACTIVE | STORAGE_BYTES | AI_FACE_CALLS |
// AI_STT_MINUTES | LLM_TOKENS | EMAIL_SENT |
// OTP_SENT | EGRESS_BYTES
value BigInt
recordedAt DateTime @default(now())
periodStart DateTime
periodEnd DateTime
@@index([kindergartenId, metric, periodStart])
}
model ProviderCost {
id String @id @default(cuid())
kindergartenId String? // null = unattributed fixed cost
provider String // s3 | rekognition | whisper | resend | telegram |
// supabase | vercel-ai
costCents Int
currency String @default("USD")
occurredAt DateTime
refType String? // MediaFile | FaceRecognitionSuggestion | JobLog
refId String?
@@index([kindergartenId, provider, occurredAt])
@@index([provider, occurredAt])
}Key insight: UsageRecord and ProviderCost are independent. Usage drives billing; ProviderCost drives gross-margin reporting. You want both.
08 · Metering
You don't need a billing engine to start collecting the data that will validate this pricing model. Add these now — cheap, and de-risks the Phase-5 launch with 60-90 days of actuals.
HeadObject in MediaProcessingJobService. Without this, storage billing is dead on arrival.ProviderCost. Free win.costCents=0. Gives you a call-count distribution per tenant.UsageRecord(CHILDREN_ACTIVE). Cron at midnight UTC. Drives tier-band detection.UsageRecord(LLM_TOKENS).${kindergartenId}/... makes this trivial.storageSummary to pull from ProviderCost.09 · Fintech
billingInterval = MONTHLY | ANNUAL from day one.SubscriptionStatus.PAUSED — saves churn.PromoCode for Phase-5 discounting.10 · Open questions
These are the variables most likely to invalidate the assumptions above.
apps/website-v2/src/lib/plans.ts — edit them there and the /plans page updates automatically.