Unsaid is a Flutter mobile app that generates emotionally reflective readings from a user’s input.
Instead of predicting outcomes, it interprets emotional subtext, internal tension, and what remains unsaid beneath surface language.
Full-stack Personal Project (Flutter + Firebase + Cloud Functions + Gemini)
- Demo video:
IMG_3821.MOV |
IMG_3820.MOV |
- Screenshots:
- Server-side AI orchestration (Gemini API key never exposed to client)
- Summary / Full split to reduce Firestore read cost and payload size
- Cursor-based pagination for scalable history browsing
- Soft delete lifecycle with transactional consistency
- Firebase Auth + ID token verification via Admin SDK
- Clean API layer using Cloud Functions (Node + TypeScript)
- Flutter (iOS / Android)
- BLoC state management
- Freezed (immutable models / union states)
- Firebase Authentication (Google Sign-In)
- Firebase Cloud Functions (Node + TypeScript)
- Firestore
- Firebase Admin SDK
- Google Gemini API (server-side only)
- Feature-level BLoC controls UI state transitions (loading → success/failure).
- Freezed is used for:
- Immutable state/data models
- Union/sealed-style states (e.g., idle/loading/success/failure)
- Safer pattern matching via
when/map
- Presentation: widgets/pages + BLoC
- Domain: app-level models and business rules (where applicable)
- Data: Firebase / Cloud Functions integration, DTO mapping, repositories
- Minimal, restrained UX designed to discourage repetitive prompting.
- History list uses lightweight summary payloads; detail view fetches full interpretation only when opened.
+-------------+ ID Token +--------------------+
| Flutter | -------------------> | Cloud Functions |
| App | | (API Layer) |
+-------------+ +--------------------+
|
v
+---------------+
| Firestore |
+---------------+
|
v
+---------------+
| Gemini API |
| (Server-side) |
+---------------+
- AI API key is never exposed to the client.
- Cloud Functions acts as a single orchestration layer.
- Firestore reads are minimized through summary/full split.
- Soft delete lifecycle is implemented for safer data management.
- Cursor pagination reduces read cost and avoids deep offset queries.
- Transactional writes keep summary and full documents consistent.
- User signs in with Google
- Firebase ID Token issued
- Token sent to server (Cloud Functions)
- Server verifies token via Firebase Admin SDK
- User document is upserted if missing
- User submits a question/prompt
- Server:
- Validates auth
- Reads user birth (optional)
- Calculates zodiac (optional contextual hint)
- Calls Gemini API
- Stores reading using Firestore transaction (full + summary)
- Returns structured Reading model
- Query
reading_summaries - Filter: status = active
- Cursor-based pagination
- Lightweight payload (summary only)
- Fetch readings/{id}
- Ownership validation
- Return full interpretation
Soft delete fields:
status = "deleted"
deleted_at = timestamp
Both summary and full documents are updated transactionally.
User (1) -------- (N) Reading
| |
| |
| ReadingSummary
|
+------ (1) UserSetting
{
"uid": "string",
"email": "string",
"birth": "YYYY-MM-DD | null",
"daily_question_count": 0,
"last_question_date": "YYYY-MM-DD",
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"is_marketing": false,
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "string",
"uid": "string",
"question": "string",
"interpretation": "string",
"unsaid_line": "string",
"status": "active | deleted",
"deleted_at": "timestamp | null",
"created_at": "timestamp",
"updated_at": "timestamp"
}{
"id": "string",
"uid": "string",
"question": "string",
"unsaid_line": "string",
"status": "active | deleted",
"deleted_at": "timestamp | null",
"created_at": "timestamp",
"updated_at": "timestamp"
}Creates a reading.
Request:
{
"prompt": "I'm happy but also sad."
}Response:
{
"data": {
"id": "abc123",
"question": "...",
"interpretation": "...",
"unsaid_line": "...",
"created_at": "...",
"updated_at": "..."
}
}Cursor-based pagination.
Query parameters:
- limit
- cursor
- cursor_id
Returns full reading detail.
Soft delete.
- Firebase Admin token verification
- Ownership validation for reads
- Environment-based secret management
- Soft delete lifecycle
- No client exposure of API keys
- Cursor-based pagination
- Summary documents for list view
- Transactional writes
- Avoid N+1 read patterns
- Prompt length control
- Service layer (zodiac, prompt builder)
- Utility functions
- Token verification
- Firestore transactions
- AI response validation
- Auth flow
- Delete lifecycle
- Pagination boundary testing
flutter pub get
flutter rundart run build_runner build --delete-conflicting-outputscd ios
pod install
cd ..firebase emulators:start --only functionsfirebase deploy --only functions- GEMINI_API_KEY
- GEMINI_MODEL
- Subscription model
- Enforced daily quota
- Rate limiting & abuse prevention
- Multi-language support
- Analytics-based personalization
- Prompt A/B testing
- Admin moderation dashboard
Full-stack Flutter + Firebase AI-integrated application designed and implemented independently.
This project uses publicly available free assets:
- Icons: https://iconsvg.xyz/, https://www.streamlinehq.com/icons/core-remix-free
- Animations: https://lottiefiles.com/
All assets are used under their respective licenses. If required, attribution is provided according to each asset’s license terms.